1 /* 2 * Copyright (C) 2024 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 package com.android.tradefed.util.avd; 17 18 import com.android.tradefed.log.LogUtil.CLog; 19 20 import com.google.gson.Gson; 21 22 import java.io.IOException; 23 import java.net.URI; 24 import java.net.http.HttpClient; 25 import java.net.http.HttpRequest; 26 import java.net.http.HttpResponse; 27 import java.net.http.HttpResponse.BodyHandlers; 28 import java.nio.file.Path; 29 import java.util.List; 30 31 /** 32 * Java implementation of Cuttlefish Host Orchestator API. 33 * 34 * <p>- Endpoints: 35 * https://github.com/google/android-cuttlefish/blob/main/frontend/src/host_orchestrator/orchestrator/controller.go#L56-L102 36 * - Objects: 37 * https://github.com/google/android-cuttlefish/blob/main/frontend/src/host_orchestrator/api/v1/messages.go 38 */ 39 public class HostOrchestratorClient { 40 41 // https://github.com/google/android-cuttlefish/blob/main/frontend/src/host_orchestrator/api/v1/messages.go#L104 42 public static final class Operation { 43 public String name; 44 public boolean done; 45 } 46 47 public static final class Cvd { 48 public String group; 49 public String name; 50 public String status; 51 } 52 53 public static final class ListCvdsResponse { 54 public List<Cvd> cvds; 55 } 56 buildListCvdsRequest(String baseURL)57 public static HttpRequest buildListCvdsRequest(String baseURL) { 58 return HttpRequest.newBuilder().uri(URI.create(String.format("%s/cvds", baseURL))).build(); 59 } 60 61 // https://github.com/google/android-cuttlefish/blob/fff7e3487c924435e6f6120345edf1dddb49d50b/frontend/src/host_orchestrator/orchestrator/controller.go#L78 buildGetOperationRequest(String baseURL, String name)62 public static HttpRequest buildGetOperationRequest(String baseURL, String name) { 63 return HttpRequest.newBuilder() 64 .uri(URI.create(String.format("%s/operations/%s", baseURL, name))) 65 .build(); 66 } 67 68 // https://github.com/google/android-cuttlefish/blob/fff7e3487c924435e6f6120345edf1dddb49d50b/frontend/src/host_orchestrator/orchestrator/controller.go#L82 buildGetOperationResultRequest(String baseURL, String name)69 public static HttpRequest buildGetOperationResultRequest(String baseURL, String name) { 70 return HttpRequest.newBuilder() 71 .uri(URI.create(String.format("%s/operations/%s/result", baseURL, name))) 72 .build(); 73 } 74 75 // https://github.com/google/android-cuttlefish/blob/fff7e3487c924435e6f6120345edf1dddb49d50b/frontend/src/host_orchestrator/orchestrator/controller.go#L69 buildCreateBugreportRequest(String baseURL, String group)76 public static HttpRequest buildCreateBugreportRequest(String baseURL, String group) { 77 return HttpRequest.newBuilder() 78 .uri(URI.create(String.format("%s/cvds/%s/:bugreport", baseURL, group))) 79 .POST(java.net.http.HttpRequest.BodyPublishers.noBody()) 80 .build(); 81 } 82 83 // https://github.com/google/android-cuttlefish/blob/fff7e3487c924435e6f6120345edf1dddb49d50b/frontend/src/host_orchestrator/orchestrator/controller.go#L75 buildPowerwashRequest(String baseURL, String group, String name)84 public static HttpRequest buildPowerwashRequest(String baseURL, String group, String name) { 85 return HttpRequest.newBuilder() 86 .uri(URI.create(String.format("%s/cvds/%s/%s/:powerwash", baseURL, group, name))) 87 .POST(java.net.http.HttpRequest.BodyPublishers.noBody()) 88 .build(); 89 } 90 91 // https://github.com/google/android-cuttlefish/blob/fff7e3487c924435e6f6120345edf1dddb49d50b/frontend/src/host_orchestrator/orchestrator/controller.go#L71 buildRemoveInstanceRequest( String baseURL, String group, String name)92 public static HttpRequest buildRemoveInstanceRequest( 93 String baseURL, String group, String name) { 94 return HttpRequest.newBuilder() 95 .uri(URI.create(String.format("%s/cvds/%s/%s", baseURL, group, name))) 96 .DELETE() 97 .build(); 98 } 99 100 public static interface IHoHttpClient { send(HttpRequest request)101 HttpResponse<String> send(HttpRequest request) 102 throws IOException, InterruptedException, ErrorResponseException; 103 send(HttpRequest request, Path dst)104 HttpResponse<Path> send(HttpRequest request, Path dst) 105 throws IOException, InterruptedException, ErrorResponseException; 106 } 107 108 public static final class HoHttpClient implements IHoHttpClient { 109 private final HttpClient mClient; 110 HoHttpClient()111 public HoHttpClient() { 112 mClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build(); 113 } 114 115 @Override send(HttpRequest request)116 public HttpResponse<String> send(HttpRequest request) 117 throws IOException, InterruptedException, ErrorResponseException { 118 return mClient.send(request, BodyHandlers.ofString()); 119 } 120 121 @Override send(HttpRequest request, Path dst)122 public HttpResponse<Path> send(HttpRequest request, Path dst) 123 throws IOException, InterruptedException, ErrorResponseException { 124 return mClient.send(request, BodyHandlers.ofFile(dst)); 125 } 126 } 127 128 public static final class ErrorResponseException extends Exception { 129 private final int mStatusCode; 130 private final String mBody; 131 ErrorResponseException(int statusCode, String body)132 public ErrorResponseException(int statusCode, String body) { 133 super( 134 String.format( 135 "error response with status code: %d, response body: %s", 136 statusCode, body)); 137 mStatusCode = statusCode; 138 mBody = body; 139 } 140 getStatusCode()141 public int getStatusCode() { 142 return mStatusCode; 143 } 144 getBody()145 public String getBody() { 146 return mBody; 147 } 148 } 149 sendRequest( IHoHttpClient client, HttpRequest request, Class<T> responseClass)150 public static <T> T sendRequest( 151 IHoHttpClient client, HttpRequest request, Class<T> responseClass) 152 throws IOException, InterruptedException, ErrorResponseException { 153 HttpResponse<String> res = client.send(request); 154 if (res.statusCode() != 200) { 155 throw new ErrorResponseException(res.statusCode(), res.body()); 156 } 157 return new Gson().fromJson(res.body(), responseClass); 158 } 159 saveToFile(IHoHttpClient client, HttpRequest request, Path dst)160 public static void saveToFile(IHoHttpClient client, HttpRequest request, Path dst) 161 throws IOException, InterruptedException, ErrorResponseException { 162 HttpResponse<Path> res = client.send(request, dst); 163 if (res.statusCode() != 200) { 164 throw new ErrorResponseException(res.statusCode(), ""); 165 } 166 CLog.i("Response body for \"%s\" successfully saved to \"%s\"", request.uri(), dst); 167 } 168 } 169