1 //
2 // Copyright (C) 2022 The Android Open Source Project
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15
16 #include "host/commands/test_gce_driver/gce_api.h"
17
18 #include <uuid.h>
19
20 #include <sstream>
21 #include <string>
22
23 #include <android-base/logging.h>
24 #include <android-base/strings.h>
25
26 #include "host/libs/web/credential_source.h"
27 #include "host/libs/web/curl_wrapper.h"
28
29 using android::base::Error;
30 using android::base::Result;
31
32 namespace cuttlefish {
33
OptStringMember(const Json::Value & jn,const std::string & name)34 std::optional<std::string> OptStringMember(const Json::Value& jn,
35 const std::string& name) {
36 if (!jn.isMember(name) || jn[name].type() != Json::ValueType::stringValue) {
37 return {};
38 }
39 return jn[name].asString();
40 }
41
OptObjMember(const Json::Value & jn,const std::string & name)42 const Json::Value* OptObjMember(const Json::Value& jn,
43 const std::string& name) {
44 if (!jn.isMember(name) || jn[name].type() != Json::ValueType::objectValue) {
45 return nullptr;
46 }
47 return &(jn[name]);
48 }
49
OptArrayMember(const Json::Value & jn,const std::string & name)50 const Json::Value* OptArrayMember(const Json::Value& jn,
51 const std::string& name) {
52 if (!jn.isMember(name) || jn[name].type() != Json::ValueType::arrayValue) {
53 return nullptr;
54 }
55 return &(jn[name]);
56 }
57
EnsureObjMember(Json::Value & jn,const std::string & name)58 Json::Value& EnsureObjMember(Json::Value& jn, const std::string& name) {
59 if (!jn.isMember(name) || jn[name].type() != Json::ValueType::objectValue) {
60 jn[name] = Json::Value(Json::ValueType::objectValue);
61 }
62 return jn[name];
63 }
64
EnsureArrayMember(Json::Value & jn,const std::string & name)65 Json::Value& EnsureArrayMember(Json::Value& jn, const std::string& name) {
66 if (!jn.isMember(name) || jn[name].type() != Json::ValueType::arrayValue) {
67 jn[name] = Json::Value(Json::ValueType::arrayValue);
68 }
69 return jn[name];
70 }
71
GceInstanceDisk(const Json::Value & json)72 GceInstanceDisk::GceInstanceDisk(const Json::Value& json) : data_(json){};
73
EphemeralBootDisk()74 GceInstanceDisk GceInstanceDisk::EphemeralBootDisk() {
75 Json::Value initial_json(Json::ValueType::objectValue);
76 initial_json["type"] = "PERSISTENT";
77 initial_json["boot"] = true;
78 initial_json["mode"] = "READ_WRITE";
79 initial_json["autoDelete"] = true;
80 return GceInstanceDisk(initial_json);
81 }
82
83 constexpr char kGceDiskInitParams[] = "initializeParams";
84 constexpr char kGceDiskName[] = "diskName";
Name() const85 std::optional<std::string> GceInstanceDisk::Name() const {
86 const auto& init_params = OptObjMember(data_, kGceDiskInitParams);
87 if (!init_params) {
88 return {};
89 }
90 return OptStringMember(*init_params, kGceDiskName);
91 }
Name(const std::string & source)92 GceInstanceDisk& GceInstanceDisk::Name(const std::string& source) & {
93 EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskName] = source;
94 return *this;
95 }
Name(const std::string & source)96 GceInstanceDisk GceInstanceDisk::Name(const std::string& source) && {
97 EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskName] = source;
98 return *this;
99 }
100
101 constexpr char kGceDiskSourceImage[] = "sourceImage";
SourceImage() const102 std::optional<std::string> GceInstanceDisk::SourceImage() const {
103 const auto& init_params = OptObjMember(data_, kGceDiskInitParams);
104 if (!init_params) {
105 return {};
106 }
107 return OptStringMember(*init_params, kGceDiskSourceImage);
108 }
SourceImage(const std::string & source)109 GceInstanceDisk& GceInstanceDisk::SourceImage(const std::string& source) & {
110 EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskSourceImage] = source;
111 return *this;
112 }
SourceImage(const std::string & source)113 GceInstanceDisk GceInstanceDisk::SourceImage(const std::string& source) && {
114 EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskSourceImage] = source;
115 return *this;
116 }
117
118 constexpr char kGceDiskSizeGb[] = "diskSizeGb";
SizeGb(uint64_t size)119 GceInstanceDisk& GceInstanceDisk::SizeGb(uint64_t size) & {
120 EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskSizeGb] = size;
121 return *this;
122 }
SizeGb(uint64_t size)123 GceInstanceDisk GceInstanceDisk::SizeGb(uint64_t size) && {
124 EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskSizeGb] = size;
125 return *this;
126 }
127
AsJson() const128 const Json::Value& GceInstanceDisk::AsJson() const { return data_; }
129
GceNetworkInterface(const Json::Value & data)130 GceNetworkInterface::GceNetworkInterface(const Json::Value& data)
131 : data_(data) {}
132
133 constexpr char kGceNetworkAccessConfigs[] = "accessConfigs";
Default()134 GceNetworkInterface GceNetworkInterface::Default() {
135 Json::Value json{Json::ValueType::objectValue};
136 json["network"] = "global/networks/default";
137 Json::Value accessConfig{Json::ValueType::objectValue};
138 accessConfig["type"] = "ONE_TO_ONE_NAT";
139 accessConfig["name"] = "External NAT";
140 EnsureArrayMember(json, kGceNetworkAccessConfigs).append(accessConfig);
141 return GceNetworkInterface(json);
142 }
143
144 constexpr char kGceNetworkExternalIp[] = "natIP";
ExternalIp() const145 std::optional<std::string> GceNetworkInterface::ExternalIp() const {
146 auto accessConfigs = OptArrayMember(data_, kGceNetworkAccessConfigs);
147 if (!accessConfigs || accessConfigs->size() < 1) {
148 return {};
149 }
150 if ((*accessConfigs)[0].type() != Json::ValueType::objectValue) {
151 return {};
152 }
153 return OptStringMember((*accessConfigs)[0], kGceNetworkExternalIp);
154 }
155
156 constexpr char kGceNetworkInternalIp[] = "networkIP";
InternalIp() const157 std::optional<std::string> GceNetworkInterface::InternalIp() const {
158 return OptStringMember(data_, kGceNetworkInternalIp);
159 }
160
AsJson() const161 const Json::Value& GceNetworkInterface::AsJson() const { return data_; }
162
GceInstanceInfo(const Json::Value & json)163 GceInstanceInfo::GceInstanceInfo(const Json::Value& json) : data_(json) {}
164
165 constexpr char kGceZone[] = "zone";
Zone() const166 std::optional<std::string> GceInstanceInfo::Zone() const {
167 return OptStringMember(data_, kGceZone);
168 }
Zone(const std::string & zone)169 GceInstanceInfo& GceInstanceInfo::Zone(const std::string& zone) & {
170 data_[kGceZone] = zone;
171 return *this;
172 }
Zone(const std::string & zone)173 GceInstanceInfo GceInstanceInfo::Zone(const std::string& zone) && {
174 data_[kGceZone] = zone;
175 return *this;
176 }
177
178 constexpr char kGceName[] = "name";
Name() const179 std::optional<std::string> GceInstanceInfo::Name() const {
180 return OptStringMember(data_, kGceName);
181 }
Name(const std::string & name)182 GceInstanceInfo& GceInstanceInfo::Name(const std::string& name) & {
183 data_[kGceName] = name;
184 return *this;
185 }
Name(const std::string & name)186 GceInstanceInfo GceInstanceInfo::Name(const std::string& name) && {
187 data_[kGceName] = name;
188 return *this;
189 }
190
191 constexpr char kGceMachineType[] = "machineType";
MachineType() const192 std::optional<std::string> GceInstanceInfo::MachineType() const {
193 return OptStringMember(data_, kGceMachineType);
194 }
MachineType(const std::string & type)195 GceInstanceInfo& GceInstanceInfo::MachineType(const std::string& type) & {
196 data_[kGceMachineType] = type;
197 return *this;
198 }
MachineType(const std::string & type)199 GceInstanceInfo GceInstanceInfo::MachineType(const std::string& type) && {
200 data_[kGceMachineType] = type;
201 return *this;
202 }
203
204 constexpr char kGceDisks[] = "disks";
AddDisk(const GceInstanceDisk & disk)205 GceInstanceInfo& GceInstanceInfo::AddDisk(const GceInstanceDisk& disk) & {
206 EnsureArrayMember(data_, kGceDisks).append(disk.AsJson());
207 return *this;
208 }
AddDisk(const GceInstanceDisk & disk)209 GceInstanceInfo GceInstanceInfo::AddDisk(const GceInstanceDisk& disk) && {
210 EnsureArrayMember(data_, kGceDisks).append(disk.AsJson());
211 return *this;
212 }
213
214 constexpr char kGceNetworkInterfaces[] = "networkInterfaces";
AddNetworkInterface(const GceNetworkInterface & net)215 GceInstanceInfo& GceInstanceInfo::AddNetworkInterface(
216 const GceNetworkInterface& net) & {
217 EnsureArrayMember(data_, kGceNetworkInterfaces).append(net.AsJson());
218 return *this;
219 }
AddNetworkInterface(const GceNetworkInterface & net)220 GceInstanceInfo GceInstanceInfo::AddNetworkInterface(
221 const GceNetworkInterface& net) && {
222 EnsureArrayMember(data_, kGceNetworkInterfaces).append(net.AsJson());
223 return *this;
224 }
NetworkInterfaces() const225 std::vector<GceNetworkInterface> GceInstanceInfo::NetworkInterfaces() const {
226 auto jsonNetworkInterfaces = OptArrayMember(data_, kGceNetworkInterfaces);
227 if (!jsonNetworkInterfaces) {
228 return {};
229 }
230 std::vector<GceNetworkInterface> interfaces;
231 for (const Json::Value& jsonNetworkInterface : *jsonNetworkInterfaces) {
232 interfaces.push_back(GceNetworkInterface(jsonNetworkInterface));
233 }
234 return interfaces;
235 }
236
237 constexpr char kGceMetadata[] = "metadata";
238 constexpr char kGceMetadataItems[] = "items";
239 constexpr char kGceMetadataKey[] = "key";
240 constexpr char kGceMetadataValue[] = "value";
AddMetadata(const std::string & key,const std::string & value)241 GceInstanceInfo& GceInstanceInfo::AddMetadata(const std::string& key,
242 const std::string& value) & {
243 Json::Value item{Json::ValueType::objectValue};
244 item[kGceMetadataKey] = key;
245 item[kGceMetadataValue] = value;
246 auto& metadata = EnsureObjMember(data_, kGceMetadata);
247 EnsureArrayMember(metadata, kGceMetadataItems).append(item);
248 return *this;
249 }
AddMetadata(const std::string & key,const std::string & value)250 GceInstanceInfo GceInstanceInfo::AddMetadata(const std::string& key,
251 const std::string& value) && {
252 Json::Value item{Json::ValueType::objectValue};
253 item[kGceMetadataKey] = key;
254 item[kGceMetadataValue] = value;
255 auto& metadata = EnsureObjMember(data_, kGceMetadata);
256 EnsureArrayMember(metadata, kGceMetadataItems).append(item);
257 return *this;
258 }
259
260 constexpr char kGceServiceAccounts[] = "serviceAccounts";
261 constexpr char kGceScopes[] = "scopes";
AddScope(const std::string & scope)262 GceInstanceInfo& GceInstanceInfo::AddScope(const std::string& scope) & {
263 auto& serviceAccounts = EnsureArrayMember(data_, kGceServiceAccounts);
264 if (serviceAccounts.size() == 0) {
265 serviceAccounts.append(Json::Value(Json::ValueType::objectValue));
266 }
267 serviceAccounts[0]["email"] = "default";
268 auto& scopes = EnsureArrayMember(serviceAccounts[0], kGceScopes);
269 scopes.append(scope);
270 return *this;
271 }
AddScope(const std::string & scope)272 GceInstanceInfo GceInstanceInfo::AddScope(const std::string& scope) && {
273 auto& serviceAccounts = EnsureArrayMember(data_, kGceServiceAccounts);
274 if (serviceAccounts.size() == 0) {
275 serviceAccounts.append(Json::Value(Json::ValueType::objectValue));
276 }
277 serviceAccounts[0]["email"] = "default";
278 auto& scopes = EnsureArrayMember(serviceAccounts[0], kGceScopes);
279 scopes.append(scope);
280 return *this;
281 }
282
AsJson() const283 const Json::Value& GceInstanceInfo::AsJson() const { return data_; }
284
GceApi(CurlWrapper & curl,CredentialSource & credentials,const std::string & project)285 GceApi::GceApi(CurlWrapper& curl, CredentialSource& credentials,
286 const std::string& project)
287 : curl_(curl), credentials_(credentials), project_(project) {}
288
Headers()289 std::vector<std::string> GceApi::Headers() {
290 return {
291 "Authorization:Bearer " + credentials_.Credential(),
292 "Content-Type: application/json",
293 };
294 }
295
296 class GceApi::Operation::Impl {
297 public:
Impl(GceApi & gce_api,std::function<Result<Json::Value> ()> initial_request)298 Impl(GceApi& gce_api, std::function<Result<Json::Value>()> initial_request)
299 : gce_api_(gce_api), initial_request_(std::move(initial_request)) {
300 operation_future_ = std::async([this]() { return Run(); });
301 }
302
Run()303 Result<bool> Run() {
304 auto initial_response = initial_request_();
305 if (!initial_response.ok()) {
306 return Error() << "Initial request failed: " << initial_response.error();
307 }
308
309 auto url = OptStringMember(*initial_response, "selfLink");
310 if (!url) {
311 return Error() << "Operation " << *initial_response
312 << " was missing `selfLink` field.";
313 }
314 url = *url + "/wait";
315 running_ = true;
316
317 while (running_) {
318 auto response =
319 gce_api_.curl_.PostToJson(*url, std::string{""}, gce_api_.Headers());
320 const auto& json = response.data;
321 Json::Value errors;
322 if (auto j_error = OptObjMember(json, "error"); j_error) {
323 if (auto j_errors = OptArrayMember(*j_error, "errors"); j_errors) {
324 errors = j_errors->size() > 0 ? *j_errors : Json::Value();
325 }
326 }
327 Json::Value warnings;
328 if (auto j_warnings = OptArrayMember(json, "warnings"); j_warnings) {
329 warnings = j_warnings->size() > 0 ? *j_warnings : Json::Value();
330 }
331 LOG(DEBUG) << "Requested operation status at \"" << *url
332 << "\", received " << json;
333 if (!response.HttpSuccess() || errors != Json::Value()) {
334 return Error() << "Error accessing \"" << *url
335 << "\". Errors: " << errors
336 << ", Warnings: " << warnings;
337 }
338 if (!json.isMember("status") ||
339 json["status"].type() != Json::ValueType::stringValue) {
340 return Error() << json << " \"status\" field invalid";
341 }
342 if (json["status"] == "DONE") {
343 return true;
344 }
345 }
346 return false;
347 }
348
349 private:
350 GceApi& gce_api_;
351 std::function<Result<Json::Value>()> initial_request_;
352 bool running_;
353 std::future<Result<bool>> operation_future_;
354 friend class GceApi::Operation;
355 };
356
Operation(std::unique_ptr<GceApi::Operation::Impl> impl)357 GceApi::Operation::Operation(std::unique_ptr<GceApi::Operation::Impl> impl)
358 : impl_(std::move(impl)) {}
359
360 GceApi::Operation::~Operation() = default;
361
StopWaiting()362 void GceApi::Operation::StopWaiting() { impl_->running_ = false; }
363
Future()364 std::future<Result<bool>>& GceApi::Operation::Future() {
365 return impl_->operation_future_;
366 }
367
RandomUuid()368 static std::string RandomUuid() {
369 uuid_t uuid;
370 uuid_generate_random(uuid);
371 std::string uuid_str = "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx";
372 uuid_unparse(uuid, uuid_str.data());
373 return uuid_str;
374 }
375
376 // GCE gives back full URLs for zones, but it only wants the last part in
377 // requests
SanitizeZone(const std::string & zone)378 static std::string SanitizeZone(const std::string& zone) {
379 auto last_slash = zone.rfind("/");
380 if (last_slash == std::string::npos) {
381 return zone;
382 }
383 return zone.substr(last_slash + 1);
384 }
385
Get(const GceInstanceInfo & instance)386 std::future<Result<GceInstanceInfo>> GceApi::Get(
387 const GceInstanceInfo& instance) {
388 auto name = instance.Name();
389 if (!name) {
390 auto task = [json = instance.AsJson()]() -> Result<GceInstanceInfo> {
391 return Error() << "Missing a name for \"" << json << "\"";
392 };
393 return std::async(std::launch::deferred, task);
394 }
395 auto zone = instance.Zone();
396 if (!zone) {
397 auto task = [json = instance.AsJson()]() -> Result<GceInstanceInfo> {
398 return Error() << "Missing a zone for \"" << json << "\"";
399 };
400 return std::async(std::launch::deferred, task);
401 }
402 return Get(*zone, *name);
403 }
404
Get(const std::string & zone,const std::string & name)405 std::future<Result<GceInstanceInfo>> GceApi::Get(const std::string& zone,
406 const std::string& name) {
407 std::stringstream url;
408 url << "https://compute.googleapis.com/compute/v1";
409 url << "/projects/" << curl_.UrlEscape(project_);
410 url << "/zones/" << curl_.UrlEscape(SanitizeZone(zone));
411 url << "/instances/" << curl_.UrlEscape(name);
412 auto task = [this, url = url.str()]() -> Result<GceInstanceInfo> {
413 auto response = curl_.DownloadToJson(url, Headers());
414 if (!response.HttpSuccess()) {
415 return Error() << "Failed to get instance info, received "
416 << response.data << " with code " << response.http_code;
417 }
418 return GceInstanceInfo(response.data);
419 };
420 return std::async(task);
421 }
422
Insert(const Json::Value & request)423 GceApi::Operation GceApi::Insert(const Json::Value& request) {
424 if (!request.isMember("zone") ||
425 request["zone"].type() != Json::ValueType::stringValue) {
426 auto task = [request]() -> Result<Json::Value> {
427 return Error() << "Missing a zone for \"" << request << "\"";
428 };
429 return Operation(
430 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
431 }
432 auto zone = request["zone"].asString();
433 Json::Value requestNoZone = request;
434 requestNoZone.removeMember("zone");
435 std::stringstream url;
436 url << "https://compute.googleapis.com/compute/v1";
437 url << "/projects/" << curl_.UrlEscape(project_);
438 url << "/zones/" << curl_.UrlEscape(SanitizeZone(zone));
439 url << "/instances";
440 url << "?requestId=" << RandomUuid(); // Avoid duplication on request retry
441 auto task = [this, requestNoZone, url = url.str()]() -> Result<Json::Value> {
442 auto response = curl_.PostToJson(url, requestNoZone, Headers());
443 if (!response.HttpSuccess()) {
444 return Error() << "Failed to create instance: " << response.data
445 << ". Sent request " << requestNoZone;
446 }
447 return response.data;
448 };
449 return Operation(
450 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
451 }
452
Insert(const GceInstanceInfo & request)453 GceApi::Operation GceApi::Insert(const GceInstanceInfo& request) {
454 return Insert(request.AsJson());
455 }
456
Reset(const std::string & zone,const std::string & name)457 GceApi::Operation GceApi::Reset(const std::string& zone,
458 const std::string& name) {
459 std::stringstream url;
460 url << "https://compute.googleapis.com/compute/v1";
461 url << "/projects/" << curl_.UrlEscape(project_);
462 url << "/zones/" << curl_.UrlEscape(SanitizeZone(zone));
463 url << "/instances/" << curl_.UrlEscape(name);
464 url << "/reset";
465 url << "?requestId=" << RandomUuid(); // Avoid duplication on request retry
466 auto task = [this, url = url.str()]() -> Result<Json::Value> {
467 auto response = curl_.PostToJson(url, Json::Value(), Headers());
468 if (!response.HttpSuccess()) {
469 return Error() << "Failed to create instance: " << response.data;
470 }
471 return response.data;
472 };
473 return Operation(
474 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
475 }
476
Reset(const GceInstanceInfo & instance)477 GceApi::Operation GceApi::Reset(const GceInstanceInfo& instance) {
478 auto name = instance.Name();
479 if (!name) {
480 auto task = [json = instance.AsJson()]() -> Result<Json::Value> {
481 return Error() << "Missing a name for \"" << json << "\"";
482 };
483 return Operation(
484 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
485 }
486 auto zone = instance.Zone();
487 if (!zone) {
488 auto task = [json = instance.AsJson()]() -> Result<Json::Value> {
489 return Error() << "Missing a zone for \"" << json << "\"";
490 };
491 return Operation(
492 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
493 }
494 return Reset(*zone, *name);
495 }
496
Delete(const std::string & zone,const std::string & name)497 GceApi::Operation GceApi::Delete(const std::string& zone,
498 const std::string& name) {
499 std::stringstream url;
500 url << "https://compute.googleapis.com/compute/v1";
501 url << "/projects/" << curl_.UrlEscape(project_);
502 url << "/zones/" << curl_.UrlEscape(SanitizeZone(zone));
503 url << "/instances/" << curl_.UrlEscape(name);
504 url << "?requestId=" << RandomUuid(); // Avoid duplication on request retry
505 auto task = [this, url = url.str()]() -> Result<Json::Value> {
506 auto response = curl_.DeleteToJson(url, Headers());
507 if (!response.HttpSuccess()) {
508 return Error() << "Failed to delete instance: " << response.data;
509 }
510 return response.data;
511 };
512 return Operation(
513 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
514 }
515
Delete(const GceInstanceInfo & instance)516 GceApi::Operation GceApi::Delete(const GceInstanceInfo& instance) {
517 auto name = instance.Name();
518 if (!name) {
519 auto task = [json = instance.AsJson()]() -> Result<Json::Value> {
520 return Error() << "Missing a name for \"" << json << "\"";
521 };
522 return Operation(
523 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
524 }
525 auto zone = instance.Zone();
526 if (!zone) {
527 auto task = [json = instance.AsJson()]() -> Result<Json::Value> {
528 return Error() << "Missing a zone for \"" << json << "\"";
529 };
530 return Operation(
531 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
532 }
533 return Delete(*zone, *name);
534 }
535
536 } // namespace cuttlefish
537