blob: 4922c593e6c33e07a3dc245b269cd919499d11b3 [file] [log] [blame]
//
// 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 <uuid.h>
#include <sstream>
#include <string>
#include <android-base/logging.h>
#include <android-base/strings.h>
#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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<GceNetworkInterface> GceInstanceInfo::NetworkInterfaces() const {
auto jsonNetworkInterfaces = OptArrayMember(data_, kGceNetworkInterfaces);
if (!jsonNetworkInterfaces) {
return {};
}
std::vector<GceNetworkInterface> 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<std::string> GceApi::Headers() {
return {
"Authorization:Bearer " + credentials_.Credential(),
"Content-Type: application/json",
};
}
class GceApi::Operation::Impl {
public:
Impl(GceApi& gce_api, std::function<Result<Json::Value>()> initial_request)
: gce_api_(gce_api), initial_request_(std::move(initial_request)) {
operation_future_ = std::async([this]() { return Run(); });
}
Result<bool> 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<Result<Json::Value>()> initial_request_;
bool running_;
std::future<Result<bool>> operation_future_;
friend class GceApi::Operation;
};
GceApi::Operation::Operation(std::unique_ptr<GceApi::Operation::Impl> impl)
: impl_(std::move(impl)) {}
GceApi::Operation::~Operation() = default;
void GceApi::Operation::StopWaiting() { impl_->running_ = false; }
std::future<Result<bool>>& 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<Result<GceInstanceInfo>> GceApi::Get(
const GceInstanceInfo& instance) {
auto name = instance.Name();
if (!name) {
auto task = [json = instance.AsJson()]() -> Result<GceInstanceInfo> {
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<GceInstanceInfo> {
return Error() << "Missing a zone for \"" << json << "\"";
};
return std::async(std::launch::deferred, task);
}
return Get(*zone, *name);
}
std::future<Result<GceInstanceInfo>> 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<GceInstanceInfo> {
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<Json::Value> {
return Error() << "Missing a zone for \"" << request << "\"";
};
return Operation(
std::unique_ptr<Operation::Impl>(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<Json::Value> {
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<Operation::Impl>(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<Json::Value> {
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<Operation::Impl>(new Operation::Impl(*this, task)));
}
GceApi::Operation GceApi::Reset(const GceInstanceInfo& instance) {
auto name = instance.Name();
if (!name) {
auto task = [json = instance.AsJson()]() -> Result<Json::Value> {
return Error() << "Missing a name for \"" << json << "\"";
};
return Operation(
std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
}
auto zone = instance.Zone();
if (!zone) {
auto task = [json = instance.AsJson()]() -> Result<Json::Value> {
return Error() << "Missing a zone for \"" << json << "\"";
};
return Operation(
std::unique_ptr<Operation::Impl>(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<Json::Value> {
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<Operation::Impl>(new Operation::Impl(*this, task)));
}
GceApi::Operation GceApi::Delete(const GceInstanceInfo& instance) {
auto name = instance.Name();
if (!name) {
auto task = [json = instance.AsJson()]() -> Result<Json::Value> {
return Error() << "Missing a name for \"" << json << "\"";
};
return Operation(
std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
}
auto zone = instance.Zone();
if (!zone) {
auto task = [json = instance.AsJson()]() -> Result<Json::Value> {
return Error() << "Missing a zone for \"" << json << "\"";
};
return Operation(
std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
}
return Delete(*zone, *name);
}
} // namespace cuttlefish