• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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