// // Copyright (C) 2022 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "host/commands/test_gce_driver/gce_api.h" #include #include #include #include #include #include "host/libs/web/credential_source.h" #include "host/libs/web/curl_wrapper.h" using android::base::Error; using android::base::Result; namespace cuttlefish { std::optional OptStringMember(const Json::Value& jn, const std::string& name) { if (!jn.isMember(name) || jn[name].type() != Json::ValueType::stringValue) { return {}; } return jn[name].asString(); } const Json::Value* OptObjMember(const Json::Value& jn, const std::string& name) { if (!jn.isMember(name) || jn[name].type() != Json::ValueType::objectValue) { return nullptr; } return &(jn[name]); } const Json::Value* OptArrayMember(const Json::Value& jn, const std::string& name) { if (!jn.isMember(name) || jn[name].type() != Json::ValueType::arrayValue) { return nullptr; } return &(jn[name]); } Json::Value& EnsureObjMember(Json::Value& jn, const std::string& name) { if (!jn.isMember(name) || jn[name].type() != Json::ValueType::objectValue) { jn[name] = Json::Value(Json::ValueType::objectValue); } return jn[name]; } Json::Value& EnsureArrayMember(Json::Value& jn, const std::string& name) { if (!jn.isMember(name) || jn[name].type() != Json::ValueType::arrayValue) { jn[name] = Json::Value(Json::ValueType::arrayValue); } return jn[name]; } GceInstanceDisk::GceInstanceDisk(const Json::Value& json) : data_(json){}; GceInstanceDisk GceInstanceDisk::EphemeralBootDisk() { Json::Value initial_json(Json::ValueType::objectValue); initial_json["type"] = "PERSISTENT"; initial_json["boot"] = true; initial_json["mode"] = "READ_WRITE"; initial_json["autoDelete"] = true; return GceInstanceDisk(initial_json); } constexpr char kGceDiskInitParams[] = "initializeParams"; constexpr char kGceDiskName[] = "diskName"; std::optional GceInstanceDisk::Name() const { const auto& init_params = OptObjMember(data_, kGceDiskInitParams); if (!init_params) { return {}; } return OptStringMember(*init_params, kGceDiskName); } GceInstanceDisk& GceInstanceDisk::Name(const std::string& source) & { EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskName] = source; return *this; } GceInstanceDisk GceInstanceDisk::Name(const std::string& source) && { EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskName] = source; return *this; } constexpr char kGceDiskSourceImage[] = "sourceImage"; std::optional GceInstanceDisk::SourceImage() const { const auto& init_params = OptObjMember(data_, kGceDiskInitParams); if (!init_params) { return {}; } return OptStringMember(*init_params, kGceDiskSourceImage); } GceInstanceDisk& GceInstanceDisk::SourceImage(const std::string& source) & { EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskSourceImage] = source; return *this; } GceInstanceDisk GceInstanceDisk::SourceImage(const std::string& source) && { EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskSourceImage] = source; return *this; } constexpr char kGceDiskSizeGb[] = "diskSizeGb"; GceInstanceDisk& GceInstanceDisk::SizeGb(uint64_t size) & { EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskSizeGb] = size; return *this; } GceInstanceDisk GceInstanceDisk::SizeGb(uint64_t size) && { EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskSizeGb] = size; return *this; } const Json::Value& GceInstanceDisk::AsJson() const { return data_; } GceNetworkInterface::GceNetworkInterface(const Json::Value& data) : data_(data) {} constexpr char kGceNetworkAccessConfigs[] = "accessConfigs"; GceNetworkInterface GceNetworkInterface::Default() { Json::Value json{Json::ValueType::objectValue}; json["network"] = "global/networks/default"; Json::Value accessConfig{Json::ValueType::objectValue}; accessConfig["type"] = "ONE_TO_ONE_NAT"; accessConfig["name"] = "External NAT"; EnsureArrayMember(json, kGceNetworkAccessConfigs).append(accessConfig); return GceNetworkInterface(json); } constexpr char kGceNetworkExternalIp[] = "natIP"; std::optional GceNetworkInterface::ExternalIp() const { auto accessConfigs = OptArrayMember(data_, kGceNetworkAccessConfigs); if (!accessConfigs || accessConfigs->size() < 1) { return {}; } if ((*accessConfigs)[0].type() != Json::ValueType::objectValue) { return {}; } return OptStringMember((*accessConfigs)[0], kGceNetworkExternalIp); } constexpr char kGceNetworkInternalIp[] = "networkIP"; std::optional GceNetworkInterface::InternalIp() const { return OptStringMember(data_, kGceNetworkInternalIp); } const Json::Value& GceNetworkInterface::AsJson() const { return data_; } GceInstanceInfo::GceInstanceInfo(const Json::Value& json) : data_(json) {} constexpr char kGceZone[] = "zone"; std::optional GceInstanceInfo::Zone() const { return OptStringMember(data_, kGceZone); } GceInstanceInfo& GceInstanceInfo::Zone(const std::string& zone) & { data_[kGceZone] = zone; return *this; } GceInstanceInfo GceInstanceInfo::Zone(const std::string& zone) && { data_[kGceZone] = zone; return *this; } constexpr char kGceName[] = "name"; std::optional GceInstanceInfo::Name() const { return OptStringMember(data_, kGceName); } GceInstanceInfo& GceInstanceInfo::Name(const std::string& name) & { data_[kGceName] = name; return *this; } GceInstanceInfo GceInstanceInfo::Name(const std::string& name) && { data_[kGceName] = name; return *this; } constexpr char kGceMachineType[] = "machineType"; std::optional GceInstanceInfo::MachineType() const { return OptStringMember(data_, kGceMachineType); } GceInstanceInfo& GceInstanceInfo::MachineType(const std::string& type) & { data_[kGceMachineType] = type; return *this; } GceInstanceInfo GceInstanceInfo::MachineType(const std::string& type) && { data_[kGceMachineType] = type; return *this; } constexpr char kGceDisks[] = "disks"; GceInstanceInfo& GceInstanceInfo::AddDisk(const GceInstanceDisk& disk) & { EnsureArrayMember(data_, kGceDisks).append(disk.AsJson()); return *this; } GceInstanceInfo GceInstanceInfo::AddDisk(const GceInstanceDisk& disk) && { EnsureArrayMember(data_, kGceDisks).append(disk.AsJson()); return *this; } constexpr char kGceNetworkInterfaces[] = "networkInterfaces"; GceInstanceInfo& GceInstanceInfo::AddNetworkInterface( const GceNetworkInterface& net) & { EnsureArrayMember(data_, kGceNetworkInterfaces).append(net.AsJson()); return *this; } GceInstanceInfo GceInstanceInfo::AddNetworkInterface( const GceNetworkInterface& net) && { EnsureArrayMember(data_, kGceNetworkInterfaces).append(net.AsJson()); return *this; } std::vector GceInstanceInfo::NetworkInterfaces() const { auto jsonNetworkInterfaces = OptArrayMember(data_, kGceNetworkInterfaces); if (!jsonNetworkInterfaces) { return {}; } std::vector interfaces; for (const Json::Value& jsonNetworkInterface : *jsonNetworkInterfaces) { interfaces.push_back(GceNetworkInterface(jsonNetworkInterface)); } return interfaces; } constexpr char kGceMetadata[] = "metadata"; constexpr char kGceMetadataItems[] = "items"; constexpr char kGceMetadataKey[] = "key"; constexpr char kGceMetadataValue[] = "value"; GceInstanceInfo& GceInstanceInfo::AddMetadata(const std::string& key, const std::string& value) & { Json::Value item{Json::ValueType::objectValue}; item[kGceMetadataKey] = key; item[kGceMetadataValue] = value; auto& metadata = EnsureObjMember(data_, kGceMetadata); EnsureArrayMember(metadata, kGceMetadataItems).append(item); return *this; } GceInstanceInfo GceInstanceInfo::AddMetadata(const std::string& key, const std::string& value) && { Json::Value item{Json::ValueType::objectValue}; item[kGceMetadataKey] = key; item[kGceMetadataValue] = value; auto& metadata = EnsureObjMember(data_, kGceMetadata); EnsureArrayMember(metadata, kGceMetadataItems).append(item); return *this; } constexpr char kGceServiceAccounts[] = "serviceAccounts"; constexpr char kGceScopes[] = "scopes"; GceInstanceInfo& GceInstanceInfo::AddScope(const std::string& scope) & { auto& serviceAccounts = EnsureArrayMember(data_, kGceServiceAccounts); if (serviceAccounts.size() == 0) { serviceAccounts.append(Json::Value(Json::ValueType::objectValue)); } serviceAccounts[0]["email"] = "default"; auto& scopes = EnsureArrayMember(serviceAccounts[0], kGceScopes); scopes.append(scope); return *this; } GceInstanceInfo GceInstanceInfo::AddScope(const std::string& scope) && { auto& serviceAccounts = EnsureArrayMember(data_, kGceServiceAccounts); if (serviceAccounts.size() == 0) { serviceAccounts.append(Json::Value(Json::ValueType::objectValue)); } serviceAccounts[0]["email"] = "default"; auto& scopes = EnsureArrayMember(serviceAccounts[0], kGceScopes); scopes.append(scope); return *this; } const Json::Value& GceInstanceInfo::AsJson() const { return data_; } GceApi::GceApi(CurlWrapper& curl, CredentialSource& credentials, const std::string& project) : curl_(curl), credentials_(credentials), project_(project) {} std::vector GceApi::Headers() { return { "Authorization:Bearer " + credentials_.Credential(), "Content-Type: application/json", }; } class GceApi::Operation::Impl { public: Impl(GceApi& gce_api, std::function()> initial_request) : gce_api_(gce_api), initial_request_(std::move(initial_request)) { operation_future_ = std::async([this]() { return Run(); }); } Result Run() { auto initial_response = initial_request_(); if (!initial_response.ok()) { return Error() << "Initial request failed: " << initial_response.error(); } auto url = OptStringMember(*initial_response, "selfLink"); if (!url) { return Error() << "Operation " << *initial_response << " was missing `selfLink` field."; } url = *url + "/wait"; running_ = true; while (running_) { auto response = gce_api_.curl_.PostToJson(*url, std::string{""}, gce_api_.Headers()); const auto& json = response.data; Json::Value errors; if (auto j_error = OptObjMember(json, "error"); j_error) { if (auto j_errors = OptArrayMember(*j_error, "errors"); j_errors) { errors = j_errors->size() > 0 ? *j_errors : Json::Value(); } } Json::Value warnings; if (auto j_warnings = OptArrayMember(json, "warnings"); j_warnings) { warnings = j_warnings->size() > 0 ? *j_warnings : Json::Value(); } LOG(DEBUG) << "Requested operation status at \"" << *url << "\", received " << json; if (!response.HttpSuccess() || errors != Json::Value()) { return Error() << "Error accessing \"" << *url << "\". Errors: " << errors << ", Warnings: " << warnings; } if (!json.isMember("status") || json["status"].type() != Json::ValueType::stringValue) { return Error() << json << " \"status\" field invalid"; } if (json["status"] == "DONE") { return true; } } return false; } private: GceApi& gce_api_; std::function()> initial_request_; bool running_; std::future> operation_future_; friend class GceApi::Operation; }; GceApi::Operation::Operation(std::unique_ptr impl) : impl_(std::move(impl)) {} GceApi::Operation::~Operation() = default; void GceApi::Operation::StopWaiting() { impl_->running_ = false; } std::future>& GceApi::Operation::Future() { return impl_->operation_future_; } static std::string RandomUuid() { uuid_t uuid; uuid_generate_random(uuid); std::string uuid_str = "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx"; uuid_unparse(uuid, uuid_str.data()); return uuid_str; } // GCE gives back full URLs for zones, but it only wants the last part in // requests static std::string SanitizeZone(const std::string& zone) { auto last_slash = zone.rfind("/"); if (last_slash == std::string::npos) { return zone; } return zone.substr(last_slash + 1); } std::future> GceApi::Get( const GceInstanceInfo& instance) { auto name = instance.Name(); if (!name) { auto task = [json = instance.AsJson()]() -> Result { return Error() << "Missing a name for \"" << json << "\""; }; return std::async(std::launch::deferred, task); } auto zone = instance.Zone(); if (!zone) { auto task = [json = instance.AsJson()]() -> Result { return Error() << "Missing a zone for \"" << json << "\""; }; return std::async(std::launch::deferred, task); } return Get(*zone, *name); } std::future> GceApi::Get(const std::string& zone, const std::string& name) { std::stringstream url; url << "https://compute.googleapis.com/compute/v1"; url << "/projects/" << curl_.UrlEscape(project_); url << "/zones/" << curl_.UrlEscape(SanitizeZone(zone)); url << "/instances/" << curl_.UrlEscape(name); auto task = [this, url = url.str()]() -> Result { auto response = curl_.DownloadToJson(url, Headers()); if (!response.HttpSuccess()) { return Error() << "Failed to get instance info, received " << response.data << " with code " << response.http_code; } return GceInstanceInfo(response.data); }; return std::async(task); } GceApi::Operation GceApi::Insert(const Json::Value& request) { if (!request.isMember("zone") || request["zone"].type() != Json::ValueType::stringValue) { auto task = [request]() -> Result { return Error() << "Missing a zone for \"" << request << "\""; }; return Operation( std::unique_ptr(new Operation::Impl(*this, task))); } auto zone = request["zone"].asString(); Json::Value requestNoZone = request; requestNoZone.removeMember("zone"); std::stringstream url; url << "https://compute.googleapis.com/compute/v1"; url << "/projects/" << curl_.UrlEscape(project_); url << "/zones/" << curl_.UrlEscape(SanitizeZone(zone)); url << "/instances"; url << "?requestId=" << RandomUuid(); // Avoid duplication on request retry auto task = [this, requestNoZone, url = url.str()]() -> Result { auto response = curl_.PostToJson(url, requestNoZone, Headers()); if (!response.HttpSuccess()) { return Error() << "Failed to create instance: " << response.data << ". Sent request " << requestNoZone; } return response.data; }; return Operation( std::unique_ptr(new Operation::Impl(*this, task))); } GceApi::Operation GceApi::Insert(const GceInstanceInfo& request) { return Insert(request.AsJson()); } GceApi::Operation GceApi::Reset(const std::string& zone, const std::string& name) { std::stringstream url; url << "https://compute.googleapis.com/compute/v1"; url << "/projects/" << curl_.UrlEscape(project_); url << "/zones/" << curl_.UrlEscape(SanitizeZone(zone)); url << "/instances/" << curl_.UrlEscape(name); url << "/reset"; url << "?requestId=" << RandomUuid(); // Avoid duplication on request retry auto task = [this, url = url.str()]() -> Result { auto response = curl_.PostToJson(url, Json::Value(), Headers()); if (!response.HttpSuccess()) { return Error() << "Failed to create instance: " << response.data; } return response.data; }; return Operation( std::unique_ptr(new Operation::Impl(*this, task))); } GceApi::Operation GceApi::Reset(const GceInstanceInfo& instance) { auto name = instance.Name(); if (!name) { auto task = [json = instance.AsJson()]() -> Result { return Error() << "Missing a name for \"" << json << "\""; }; return Operation( std::unique_ptr(new Operation::Impl(*this, task))); } auto zone = instance.Zone(); if (!zone) { auto task = [json = instance.AsJson()]() -> Result { return Error() << "Missing a zone for \"" << json << "\""; }; return Operation( std::unique_ptr(new Operation::Impl(*this, task))); } return Reset(*zone, *name); } GceApi::Operation GceApi::Delete(const std::string& zone, const std::string& name) { std::stringstream url; url << "https://compute.googleapis.com/compute/v1"; url << "/projects/" << curl_.UrlEscape(project_); url << "/zones/" << curl_.UrlEscape(SanitizeZone(zone)); url << "/instances/" << curl_.UrlEscape(name); url << "?requestId=" << RandomUuid(); // Avoid duplication on request retry auto task = [this, url = url.str()]() -> Result { auto response = curl_.DeleteToJson(url, Headers()); if (!response.HttpSuccess()) { return Error() << "Failed to delete instance: " << response.data; } return response.data; }; return Operation( std::unique_ptr(new Operation::Impl(*this, task))); } GceApi::Operation GceApi::Delete(const GceInstanceInfo& instance) { auto name = instance.Name(); if (!name) { auto task = [json = instance.AsJson()]() -> Result { return Error() << "Missing a name for \"" << json << "\""; }; return Operation( std::unique_ptr(new Operation::Impl(*this, task))); } auto zone = instance.Zone(); if (!zone) { auto task = [json = instance.AsJson()]() -> Result { return Error() << "Missing a zone for \"" << json << "\""; }; return Operation( std::unique_ptr(new Operation::Impl(*this, task))); } return Delete(*zone, *name); } } // namespace cuttlefish