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.device.cloud; 17 18 import com.android.ddmlib.Log.LogLevel; 19 import com.android.tradefed.device.ITestDevice; 20 import com.android.tradefed.device.cloud.OxygenClient.LHPTunnelMode; 21 import com.android.tradefed.log.LogUtil.CLog; 22 import com.android.tradefed.util.CommandResult; 23 import com.android.tradefed.util.CommandStatus; 24 import com.android.tradefed.util.IRunUtil; 25 import com.android.tradefed.util.RunUtil; 26 import com.android.tradefed.util.ZipUtil2; 27 import com.google.common.annotations.VisibleForTesting; 28 import java.io.File; 29 import java.io.IOException; 30 import java.nio.file.Files; 31 import java.util.ArrayList; 32 import java.util.List; 33 import org.json.JSONArray; 34 import org.json.JSONException; 35 import org.json.JSONObject; 36 import org.json.JSONTokener; 37 38 /** Utility to execute commands via Host Orchestrator on remote instances. */ 39 public class HostOrchestratorUtil { 40 private static final long CMD_TIMEOUT_MS = 6 * 5 * 1000 * 1000; // 5 min 41 private static final String OXYGEN_TUNNEL_PARAM = "-L%s:127.0.0.1:2080"; 42 private static final String HO_BASE_URL = "http://%s:%s/%s"; 43 private static final String HO_PULL_LOG = "runtimeartifacts/:pull"; 44 private static final String HO_POWERWASH = "cvds/%s/%s/:powerwash"; 45 private static final String CVD_HOST_LOGZ = "cvd_hostlog_zip"; 46 private static final String UNSUPPORTED_API_RESPONSE = "404 page not found"; 47 private ITestDevice mDevice; 48 private GceAvdInfo mGceAvd; 49 private OxygenClient mOxygenClient; 50 HostOrchestratorUtil(ITestDevice device, GceAvdInfo gceAvd)51 public HostOrchestratorUtil(ITestDevice device, GceAvdInfo gceAvd) { 52 this(device, gceAvd, new OxygenClient(device.getOptions().getAvdDriverBinary())); 53 } 54 HostOrchestratorUtil(ITestDevice device, GceAvdInfo gceAvd, OxygenClient oxygenClient)55 public HostOrchestratorUtil(ITestDevice device, GceAvdInfo gceAvd, OxygenClient oxygenClient) { 56 mDevice = device; 57 mGceAvd = gceAvd; 58 mOxygenClient = oxygenClient; 59 } 60 61 /** Pull CF host logs via Host Orchestrator. */ pullCvdHostLogs()62 public File pullCvdHostLogs() { 63 // Basically, the rough processes to pull CF host logs are 64 // 1. Portforward the CURL tunnel 65 // 2. Compose CURL command and execute it to pull CF logs. 66 // TODO(easoncylee): Flesh out this section when it's ready. 67 String portNumber = Integer.toString(mOxygenClient.createServerSocket()); 68 Process tunnel = null; 69 File cvdLogsDir = null; 70 File cvdLogsZip = null; 71 try { 72 cvdLogsZip = Files.createTempFile(CVD_HOST_LOGZ, ".zip").toFile(); 73 tunnel = createHostOrchestratorTunnel(portNumber); 74 if (tunnel == null || !tunnel.isAlive()) { 75 CLog.e("Failed portforwarding Host Orchestrator tunnel."); 76 return null; 77 } 78 CommandResult commandRes = 79 curlCommandExecution( 80 mGceAvd.hostAndPort().getHost(), 81 portNumber, 82 "POST", 83 HO_PULL_LOG, 84 "--output", 85 cvdLogsZip.getAbsolutePath()); 86 if (!CommandStatus.SUCCESS.equals(commandRes.getStatus())) { 87 CLog.e("Failed pulling cvd logs via Host Orchestrator: %s", commandRes.getStdout()); 88 return null; 89 } 90 cvdLogsDir = ZipUtil2.extractZipToTemp(cvdLogsZip, "cvd_logs"); 91 } catch (IOException e) { 92 CLog.e("Failed pulling cvd logs via Host Orchestrator: %s", e); 93 } finally { 94 mOxygenClient.closeLHPConnection(tunnel); 95 cvdLogsZip.delete(); 96 } 97 return cvdLogsDir; 98 } 99 100 /** 101 * Attempt to powerwash a GCE instance via Host Orchestrator. 102 * 103 * @return A {@link CommandResult} containing the status and logs. 104 */ powerwashGce()105 public CommandResult powerwashGce() { 106 // Basically, the rough processes to powerwash a GCE instance are 107 // 1. Portforward CURL tunnel 108 // 2. Obtain the necessary information to powerwash a GCE instance via Host Orchestrator. 109 // 3. Attempt to powerwash a GCE instance via Host Orchestrator. 110 // TODO(easoncylee): Flesh out this section when it's ready. 111 String portNumber = Integer.toString(mOxygenClient.createServerSocket()); 112 Process tunnel = null; 113 CommandResult curlRes = new CommandResult(CommandStatus.EXCEPTION); 114 try { 115 tunnel = createHostOrchestratorTunnel(portNumber); 116 if (tunnel == null || !tunnel.isAlive()) { 117 String msg = "Failed portforwarding Host Orchestrator tunnel."; 118 CLog.e(msg); 119 curlRes.setStderr(msg); 120 return curlRes; 121 } 122 curlRes = 123 curlCommandExecution( 124 mGceAvd.hostAndPort().getHost(), portNumber, "GET", "cvds"); 125 if (!CommandStatus.SUCCESS.equals(curlRes.getStatus())) { 126 CLog.e("Failed getting cvd status via Host Orchestrator: %s", curlRes.getStdout()); 127 return curlRes; 128 } 129 String cvdGroup = parseCvdOutput(curlRes.getStdout(), "group"); 130 String cvdName = parseCvdOutput(curlRes.getStdout(), "name"); 131 if (cvdGroup == null || cvdGroup.isEmpty() || cvdName == null || cvdName.isEmpty()) { 132 CLog.e("Failed parsing cvd group and cvd name."); 133 curlRes.setStatus(CommandStatus.FAILED); 134 return curlRes; 135 } 136 curlRes = 137 curlCommandExecution( 138 mGceAvd.hostAndPort().getHost(), 139 portNumber, 140 "POST", 141 String.format(HO_POWERWASH, cvdGroup, cvdName)); 142 if (!CommandStatus.SUCCESS.equals(curlRes.getStatus())) { 143 CLog.e("Failed powerwashing cvd via Host Orchestrator: %s", curlRes.getStdout()); 144 } 145 } catch (IOException e) { 146 CLog.e("Failed powerwashing gce via Host Orchestrator: %s", e); 147 } finally { 148 mOxygenClient.closeLHPConnection(tunnel); 149 } 150 return curlRes; 151 } 152 153 /** Attempt to stop a Cuttlefish instance via Host Orchestrator. */ stopGce()154 public CommandResult stopGce() { 155 // TODO(b/339304559): Flesh out this section when the host orchestrator is supported. 156 return new CommandResult(CommandStatus.EXCEPTION); 157 } 158 159 /** Attempt to snapshot a Cuttlefish instance via Host Orchestrator. */ snapshotGce()160 public CommandResult snapshotGce() { 161 // TODO(b/339304559): Flesh out this section when the host orchestrator is supported. 162 return new CommandResult(CommandStatus.EXCEPTION); 163 } 164 165 /** Attempt to restore snapshot of a Cuttlefish instance via Host Orchestrator. */ restoreSnapshotGce()166 public CommandResult restoreSnapshotGce() { 167 // TODO(b/339304559): Flesh out this section when the host orchestrator is supported. 168 return new CommandResult(CommandStatus.EXCEPTION); 169 } 170 171 /** 172 * Create Host Orchestrator Tunnel with a given port number. 173 * 174 * @param portNumber The port number that Host Orchestrator communicates with. 175 * @return A {@link Process} of the Host Orchestrator connection between CuttleFish and TF. 176 */ 177 @VisibleForTesting createHostOrchestratorTunnel(String portNumber)178 Process createHostOrchestratorTunnel(String portNumber) throws IOException { 179 // Basically, to portforwad the CURL tunnel, the rough process would be 180 // if it's oxygenation device -> portforward the CURL tunnel via LHP. 181 // if `use_cvd` is set -> portforward the CURL tunnel via SSH. 182 // TODO(easoncylee): Flesh out this section when it's ready. 183 if (mDevice.getOptions().useOxygenationDevice()) { 184 CLog.d("Portforwarding Host Orchestrator service via LHP for Oxygenation CF."); 185 return mOxygenClient.createTunnelViaLHP( 186 LHPTunnelMode.CURL, 187 portNumber, 188 mGceAvd.instanceName(), 189 mGceAvd.getOxygenationDeviceId()); 190 } else if (mDevice.getOptions().getExtraOxygenArgs().containsKey("use_cvd")) { 191 CLog.d("Portforarding Host Orchestrator service via SSH tunnel for Oxygen CF."); 192 List<String> tunnelParam = new ArrayList<>(); 193 tunnelParam.add(String.format(OXYGEN_TUNNEL_PARAM, portNumber)); 194 tunnelParam.add("-N"); 195 List<String> cmd = 196 GceRemoteCmdFormatter.getSshCommand( 197 mDevice.getOptions().getSshPrivateKeyPath(), 198 tunnelParam, 199 mDevice.getOptions().getInstanceUser(), 200 mGceAvd.hostAndPort().getHost(), 201 "" /* no command */); 202 return getRunUtil().runCmdInBackground(cmd); 203 } 204 CLog.d("Skip portforwarding Host Orchestrator service for neither Oxygen nor Oxygenation."); 205 return null; 206 } 207 208 /** 209 * Execute a curl command via Host Orchestrator. 210 * 211 * @param hostName The name of the host. 212 * @param portNumber The port number that Host Orchestrator communicates with. 213 * @param method The HTTP Request containing GET, POST, PUT, DELETE, PATCH, etc... 214 * @param api The API that Host Orchestrator supports. 215 * @param commands The command to be executed. 216 * @return A {@link CommandResult} containing the status and logs. 217 */ 218 @VisibleForTesting curlCommandExecution( String hostName, String portNumber, String method, String api, String... commands)219 CommandResult curlCommandExecution( 220 String hostName, String portNumber, String method, String api, String... commands) { 221 List<String> cmd = new ArrayList<>(); 222 cmd.add("curl"); 223 cmd.add("-0"); 224 cmd.add("-v"); 225 cmd.add("-X"); 226 cmd.add(method); 227 cmd.add(String.format(HO_BASE_URL, hostName, portNumber, api)); 228 for (String cmdOption : commands) { 229 cmd.add(cmdOption); 230 } 231 CommandResult commandRes = 232 getRunUtil().runTimedCmd(CMD_TIMEOUT_MS, null, null, cmd.toArray(new String[0])); 233 CLog.logAndDisplay( 234 LogLevel.INFO, 235 "Executing Host Orchestrator curl command: %s, Output: %s, Status: %s", 236 cmd, 237 commandRes.getStdout(), 238 commandRes.getStatus()); 239 if (commandRes.getStdout().contains(UNSUPPORTED_API_RESPONSE)) { 240 commandRes.setStatus(CommandStatus.FAILED); 241 } 242 return commandRes; 243 } 244 245 /** Return the return by parsing the cvd output with a given keyword. */ parseCvdOutput(String content, String keyword)246 private String parseCvdOutput(String content, String keyword) { 247 JSONTokener tokener = new JSONTokener(content); 248 String output = null; 249 try { 250 JSONObject root = new JSONObject(tokener); 251 JSONArray array = root.getJSONArray("cvds"); 252 JSONObject object = array.getJSONObject(0); 253 output = object.getString(keyword); 254 } catch (JSONException e) { 255 CLog.e(e); 256 } 257 return output; 258 } 259 260 /** Get {@link IRunUtil} to use. Exposed for unit testing. */ 261 @VisibleForTesting getRunUtil()262 IRunUtil getRunUtil() { 263 return RunUtil.getDefault(); 264 } 265 266 /** Return the unsupported api response. Exposed for unit testing. */ 267 @VisibleForTesting getUnsupportedHoResponse()268 String getUnsupportedHoResponse() { 269 return UNSUPPORTED_API_RESPONSE; 270 } 271 } 272