Add more error checking and log output to fetch_cvd

Includes:
 - Reporting Build API error messages through to the user
 - Enabling verbose debugging for curl internals
 - Validating credentials were actually received with
   "-credential_source=gce".

Bug: 137304531
Test: Run with some invalid flags.
Change-Id: Ic5483ff5a119e89b3d2072aff0b6cdadc2b4be46
diff --git a/host/commands/fetcher/build_api.cc b/host/commands/fetcher/build_api.cc
index 177c565..8dbf62b 100644
--- a/host/commands/fetcher/build_api.cc
+++ b/host/commands/fetcher/build_api.cc
@@ -52,6 +52,10 @@
   crc32 = json_artifact["crc32"].asUInt();
 }
 
+std::ostream& operator<<(std::ostream& out, const DeviceBuild& build) {
+  return out << "(id=\"" << build.id << "\", target=\"" << build.target << "\")";
+}
+
 BuildApi::BuildApi(std::unique_ptr<CredentialSource> credential_source)
     : credential_source(std::move(credential_source)) {}
 
@@ -68,8 +72,13 @@
   std::string url = BUILD_API + "/builds?branch=" + branch
       + "&buildType=submitted&maxResults=1&successful=true&target=" + target;
   auto response = curl.DownloadToJson(url, Headers());
-  if (response["builds"].size() != 1) {
-    LOG(WARNING) << "invalid number of builds\n";
+  CHECK(!response.isMember("error")) << "Error fetching the latest build of \""
+      << target << "\" on \"" << branch << "\". Response was " << response;
+
+  if (!response.isMember("builds") || response["builds"].size() != 1) {
+    LOG(WARNING) << "expected to receive 1 build for \"" << target << "\" on \""
+        << branch << "\", but received " << response["builds"].size()
+        << ". Full response was " << response;
     return "";
   }
   return response["builds"][0]["buildId"].asString();
@@ -78,6 +87,9 @@
 std::string BuildApi::BuildStatus(const DeviceBuild& build) {
   std::string url = BUILD_API + "/builds/" + build.id + "/" + build.target;
   auto response_json = curl.DownloadToJson(url, Headers());
+  CHECK(!response_json.isMember("error")) << "Error fetching the status of "
+      << "build " << build << ". Response was " << response_json;
+
   return response_json["buildAttemptStatus"].asString();
 }
 
@@ -85,6 +97,9 @@
   std::string url = BUILD_API + "/builds/" + build.id + "/" + build.target
       + "/attempts/latest/artifacts?maxResults=1000";
   auto artifacts_json = curl.DownloadToJson(url, Headers());
+  CHECK(!artifacts_json.isMember("error")) << "Error fetching the artifacts of "
+      << build << ". Response was " << artifacts_json;
+
   std::vector<Artifact> artifacts;
   for (const auto& artifact_json : artifacts_json["artifacts"]) {
     artifacts.emplace_back(artifact_json);
@@ -119,24 +134,21 @@
   if (branch_latest_build_id != "") {
     LOG(INFO) << "The latest good build on branch \"" << branch_or_id
         << "\"with build target \"" << build_target
-        << "is \"" << branch_latest_build_id << "\"";
+        << "\" is \"" << branch_latest_build_id << "\"";
     build_id = branch_latest_build_id;
   }
   DeviceBuild proposed_build = DeviceBuild(build_id, build_target);
   std::string status = build_api->BuildStatus(proposed_build);
   if (status == "") {
-    LOG(FATAL) << '"' << build_id << "\" with build target \""
-        << build_target << "\" is not a valid branch or build id.";
+    LOG(FATAL) << proposed_build << " is not a valid branch or build id.";
   }
-  LOG(INFO) << "Status for build " << build_id << "/" << build_target
-      << " is " << status;
+  LOG(INFO) << "Status for build " << proposed_build << " is " << status;
   while (retry_period != std::chrono::seconds::zero() && !StatusIsTerminal(status)) {
     LOG(INFO) << "Status is \"" << status << "\". Waiting for " << retry_period.count()
         << " seconds.";
     std::this_thread::sleep_for(retry_period);
     status = build_api->BuildStatus(proposed_build);
   }
-  LOG(INFO) << "Status for build " << build_id << "/" << build_target
-      << " is " << status;
+  LOG(INFO) << "Status for build " << proposed_build << " is " << status;
   return proposed_build;
 }
diff --git a/host/commands/fetcher/build_api.h b/host/commands/fetcher/build_api.h
index 36aa81f..bb89e81 100644
--- a/host/commands/fetcher/build_api.h
+++ b/host/commands/fetcher/build_api.h
@@ -18,6 +18,7 @@
 #include <chrono>
 #include <functional>
 #include <memory>
+#include <ostream>
 #include <string>
 
 #include "credential_source.h"
@@ -55,6 +56,8 @@
   std::string target;
 };
 
+std::ostream& operator<<(std::ostream&, const DeviceBuild&);
+
 class BuildApi {
   CurlWrapper curl;
   std::unique_ptr<CredentialSource> credential_source;
diff --git a/host/commands/fetcher/credential_source.cc b/host/commands/fetcher/credential_source.cc
index 579272f..89d6b14 100644
--- a/host/commands/fetcher/credential_source.cc
+++ b/host/commands/fetcher/credential_source.cc
@@ -15,6 +15,8 @@
 
 #include "credential_source.h"
 
+#include <glog/logging.h>
+
 namespace {
 
 std::chrono::steady_clock::duration REFRESH_WINDOW =
@@ -39,6 +41,16 @@
 void GceMetadataCredentialSource::RefreshCredential() {
   Json::Value credential_json =
       curl.DownloadToJson(REFRESH_URL, {"Metadata-Flavor: Google"});
+
+  CHECK(!credential_json.isMember("error")) << "Error fetching credentials. " <<
+      "Response was " << credential_json;
+  bool has_access_token = credential_json.isMember("access_token");
+  bool has_expires_in = credential_json.isMember("expires_in");
+  if (!has_access_token || !has_expires_in) {
+    LOG(FATAL) << "GCE credential was missing access_token or expires_in. "
+        << "Full response was " << credential_json << "";
+  }
+
   expiration = std::chrono::steady_clock::now()
       + std::chrono::seconds(credential_json["expires_in"].asInt());
   latest_credential = credential_json["access_token"].asString();
diff --git a/host/commands/fetcher/curl_wrapper.cc b/host/commands/fetcher/curl_wrapper.cc
index 93dca5e..472963a 100644
--- a/host/commands/fetcher/curl_wrapper.cc
+++ b/host/commands/fetcher/curl_wrapper.cc
@@ -78,6 +78,9 @@
   curl_easy_setopt(curl, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt");
   curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curl_headers);
   curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+  char error_buf[CURL_ERROR_SIZE];
+  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buf);
+  curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
   FILE* file = fopen(path.c_str(), "w");
   if (!file) {
     LOG(ERROR) << "could not open file " << path;
@@ -89,8 +92,11 @@
     curl_slist_free_all(curl_headers);
   }
   fclose(file);
-  if(res != CURLE_OK) {
-    LOG(ERROR) << "curl_easy_perform() failed: " << curl_easy_strerror(res);
+  if (res != CURLE_OK) {
+    LOG(ERROR) << "curl_easy_perform() failed. "
+        << "Code was \"" << res << "\". "
+        << "Strerror was \"" << curl_easy_strerror(res) << "\". "
+        << "Error buffer was \"" << error_buf << "\".";
     return false;
   }
   return true;
@@ -115,12 +121,18 @@
   std::stringstream data;
   curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, file_write_callback);
   curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
+  char error_buf[CURL_ERROR_SIZE];
+  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buf);
+  curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
   CURLcode res = curl_easy_perform(curl);
   if (curl_headers) {
     curl_slist_free_all(curl_headers);
   }
-  if(res != CURLE_OK) {
-    LOG(ERROR) << "curl_easy_perform() failed: " << curl_easy_strerror(res);
+  if (res != CURLE_OK) {
+    LOG(ERROR) << "curl_easy_perform() failed. "
+        << "Code was \"" << res << "\". "
+        << "Strerror was \"" << curl_easy_strerror(res) << "\". "
+        << "Error buffer was \"" << error_buf << "\".";
     return "";
   }
   return data.str();
@@ -137,6 +149,8 @@
   Json::Value json;
   if (!reader.parse(contents, json)) {
     LOG(ERROR) << "Could not parse json: " << reader.getFormattedErrorMessages();
+    json["error"] = "Failed to parse json.";
+    json["response"] = contents;
   }
   return json;
 }
diff --git a/host/commands/fetcher/fetch_cvd.cc b/host/commands/fetcher/fetch_cvd.cc
index cf6c4b6..021bdca 100644
--- a/host/commands/fetcher/fetch_cvd.cc
+++ b/host/commands/fetcher/fetch_cvd.cc
@@ -74,7 +74,11 @@
     return false;
   }
   std::string local_path = target_directory + "/" + img_zip_name;
-  build_api->ArtifactToFile(build, img_zip_name, local_path);
+  if (!build_api->ArtifactToFile(build, img_zip_name, local_path)) {
+    LOG(ERROR) << "Unable to download " << build << ":" << img_zip_name << " to "
+        << local_path;
+    return false;
+  }
 
   auto could_extract = ExtractImages(local_path, target_directory, images);
   if (!could_extract) {
@@ -104,7 +108,13 @@
     return false;
   }
   std::string local_path = target_directory + "/" + HOST_TOOLS;
-  build_api->ArtifactToFile(build, HOST_TOOLS, local_path);
+
+  if (!build_api->ArtifactToFile(build, HOST_TOOLS, local_path)) {
+    LOG(ERROR) << "Unable to download " << build << ":" << HOST_TOOLS << " to "
+        << local_path;
+    return false;
+  }
+
   if (cvd::execute({"/bin/tar", "xvf", local_path, "-C", target_directory}) != 0) {
     LOG(FATAL) << "Could not extract " << local_path;
     return false;
@@ -161,12 +171,10 @@
                                                 retry_period);
 
     if (!download_host_package(&build_api, default_build, target_dir)) {
-      LOG(FATAL) << "Could not download host package with target "
-          << default_build.target << " and build id " << default_build.id;
+      LOG(FATAL) << "Could not download host package for " << default_build;
     }
     if (!download_images(&build_api, default_build, target_dir)) {
-      LOG(FATAL) << "Could not download images with target "
-          << default_build.target << " and build id " << default_build.id;
+      LOG(FATAL) << "Could not download images for " << default_build;
     }
 
     desparse(target_dir + "/userdata.img");
@@ -178,8 +186,7 @@
 
       if (!download_images(&build_api, system_build, target_dir,
                            {"system.img"})) {
-        LOG(FATAL) << "Could not download system image at target "
-            << system_build.target << " and build id " << system_build.id;
+        LOG(FATAL) << "Could not download system image for " << system_build;
       }
     }
 
@@ -187,7 +194,10 @@
       DeviceBuild kernel_build = ArgumentToBuild(&build_api, FLAGS_kernel_build,
                                                  "kernel", retry_period);
 
-      build_api.ArtifactToFile(kernel_build, "bzImage", target_dir + "/kernel");
+      if (!build_api.ArtifactToFile(kernel_build, "bzImage", target_dir + "/kernel")) {
+        LOG(FATAL) << "Could not download " << kernel_build << ":bzImage to "
+            << target_dir + "/kernel";
+      }
     }
   }
   curl_global_cleanup();