537 lines
18 KiB
C++
537 lines
18 KiB
C++
//
|
|
// 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
|