• 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 
17 package com.android.tradefed.util.avd;
18 
19 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
20 import com.android.tradefed.log.LogUtil.CLog;
21 import com.android.tradefed.util.CommandResult;
22 import com.android.tradefed.util.CommandStatus;
23 import com.android.tradefed.util.IRunUtil;
24 import com.android.tradefed.util.MultiMap;
25 import com.android.tradefed.util.RunUtil;
26 
27 import com.google.common.annotations.VisibleForTesting;
28 import com.google.common.base.Strings;
29 import com.google.common.collect.Lists;
30 
31 import java.io.FileOutputStream;
32 import java.io.IOException;
33 import java.net.ServerSocket;
34 import java.text.DateFormat;
35 import java.text.SimpleDateFormat;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collections;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Set;
43 import java.util.concurrent.TimeUnit;
44 import java.util.stream.Collectors;
45 import java.util.stream.Stream;
46 
47 /** A class that manages the use of Oxygen client binary to lease or release Oxygen device. */
48 public class OxygenClient {
49 
50     public enum LHPTunnelMode {
51         SSH,
52         ADB,
53         CURL;
54     }
55 
56     // A list of commands to be executed to lease or release Oxygen devices, examples:
57     // 1. if the binary is an executable script, execute it directly.
58     // 2. if the binary is a jar file, execute it by using java -jar ${binary_path}.
59     private final List<String> mCmdArgs = Lists.newArrayList();
60 
61     private IRunUtil mRunUtil;
62 
63     // A list of attributes to be stored in Oxygen metadata.
64     private static final Set<String> INVOCATION_ATTRIBUTES =
65             new HashSet<>(Arrays.asList("work_unit_id"));
66 
67     // We can't be sure from GceDeviceParams use _ in options or -. This is because acloud used -
68     // in its options while Oxygen use _. For compatibility reason, this mapping is needed.
69     public static final Map<String, String> sGceDeviceParamsToOxygenMap =
70             Stream.of(
71                             new String[][] {
72                                 {"--branch", "-build_branch"},
73                                 {"--build-branch", "-build_branch"},
74                                 {"--build_branch", "-build_branch"},
75                                 {"--build-target", "-build_target"},
76                                 {"--build_target", "-build_target"},
77                                 {"--build-id", "-build_id"},
78                                 {"--build_id", "-build_id"},
79                                 {"--system-build-id", "-system_build_id"},
80                                 {"--system_build_id", "-system_build_id"},
81                                 {"--system-build-target", "-system_build_target"},
82                                 {"--system_build_target", "-system_build_target"},
83                                 {"--kernel-build-id", "-kernel_build_id"},
84                                 {"--kernel_build_id", "-kernel_build_id"},
85                                 {"--kernel-build-target", "-kernel_build_target"},
86                                 {"--kernel_build_target", "-kernel_build_target"},
87                                 {"--boot-build-id", "-boot_build_id"},
88                                 {"--boot_build_id", "-boot_build_id"},
89                                 {"--boot-build-target", "-boot_build_target"},
90                                 {"--boot_build_target", "-boot_build_target"},
91                                 {"--boot-artifact", "-boot_artifact"},
92                                 {"--boot_artifact", "-boot_artifact"},
93                                 {"--host_package_build_id", "-host_package_build_id"},
94                                 {"--host_package_build_target", "-host_package_build_target"},
95                                 {"--bootloader-build-id", "-bootloader_build_id"},
96                                 {"--bootloader_build_id", "-bootloader_build_id"},
97                                 {"--bootloader-build-target", "-bootloader_build_target"},
98                                 {"--bootloader_build_target", "-bootloader_build_target"}
99                             })
100                     .collect(
101                             Collectors.collectingAndThen(
102                                     Collectors.toMap(data -> data[0], data -> data[1]),
103                                     Collections::<String, String>unmodifiableMap));
104 
105     @VisibleForTesting
getRunUtil()106     IRunUtil getRunUtil() {
107         return mRunUtil;
108     }
109 
110     /**
111      * The constructor of OxygenClient class.
112      *
113      * @param cmdArgs a {@link List<String>} of commands to run Oxygen client.
114      * @param runUtil a {@link IRunUtil} to execute commands.
115      */
116     @VisibleForTesting
OxygenClient(List<String> cmdArgs, IRunUtil runUtil)117     OxygenClient(List<String> cmdArgs, IRunUtil runUtil) {
118         mCmdArgs.addAll(cmdArgs);
119         mRunUtil = runUtil;
120     }
121 
122     /**
123      * The constructor of OxygenClient class.
124      *
125      * @param cmdArgs a {@link List<String>} of commands to run Oxygen client.
126      */
OxygenClient(List<String> cmdArgs)127     public OxygenClient(List<String> cmdArgs) {
128         mCmdArgs.addAll(cmdArgs);
129         mRunUtil = RunUtil.getDefault();
130     }
131 
132     /**
133      * Adds invocation attributes to the given list of arguments.
134      *
135      * @param args command line args to call Oxygen client
136      * @param attributes the map of attributes to add
137      */
addInvocationAttributes(List<String> args, MultiMap<String, String> attributes)138     private void addInvocationAttributes(List<String> args, MultiMap<String, String> attributes) {
139         if (attributes == null) {
140             return;
141         }
142         List<String> debugInfo = new ArrayList<>();
143         for (Map.Entry<String, String> attr : attributes.entries()) {
144             if (INVOCATION_ATTRIBUTES.contains(attr.getKey())) {
145                 debugInfo.add(String.format("%s:%s", attr.getKey(), attr.getValue()));
146             }
147         }
148         if (debugInfo.size() > 0) {
149             args.add("-user_debug_info");
150             args.add(String.join(",", debugInfo));
151         }
152     }
153 
154     /**
155      * Attempt to lease a device by calling Oxygen client binary.
156      *
157      * @param buildTarget build target
158      * @param buildBranch build branch
159      * @param buildId build ID
160      * @param targetRegion target region for Oxygen instance
161      * @param accountingUser Oxygen accounting user email
162      * @param leaseLength number of ms for the lease duration
163      * @param gceDriverParams {@link List<String>} of gce driver params
164      * @param extraOxygenArgs {@link Map<String, String>} of extra Oxygen lease args
165      * @param attributes attributes associated with current invocation
166      * @param gceCmdTimeout number of ms for the command line timeout
167      * @param useOxygenation whether the device is leased from OmniLab Infra or not.
168      * @return a {@link CommandResult} that Oxygen binary returned.
169      */
leaseDevice( String buildTarget, String buildBranch, String buildId, String targetRegion, String accountingUser, long leaseLength, List<String> gceDriverParams, Map<String, String> extraOxygenArgs, MultiMap<String, String> attributes, long gceCmdTimeout, boolean useOxygenation)170     public CommandResult leaseDevice(
171             String buildTarget,
172             String buildBranch,
173             String buildId,
174             String targetRegion,
175             String accountingUser,
176             long leaseLength,
177             List<String> gceDriverParams,
178             Map<String, String> extraOxygenArgs,
179             MultiMap<String, String> attributes,
180             long gceCmdTimeout,
181             boolean useOxygenation) {
182         List<String> oxygenClientArgs = Lists.newArrayList(mCmdArgs);
183         oxygenClientArgs.add("-lease");
184         // Add options from GceDriverParams
185         int i = 0;
186         String branch = null;
187         Boolean buildIdSet = false;
188         while (i < gceDriverParams.size()) {
189             String gceDriverOption = gceDriverParams.get(i);
190             if (sGceDeviceParamsToOxygenMap.containsKey(gceDriverOption)) {
191                 // add device build options in oxygen's way
192                 oxygenClientArgs.add(sGceDeviceParamsToOxygenMap.get(gceDriverOption));
193                 // add option's value
194                 oxygenClientArgs.add(gceDriverParams.get(i + 1));
195                 if (gceDriverOption.equals("--branch")) {
196                     branch = gceDriverParams.get(i + 1);
197                 } else if (!buildIdSet
198                         && sGceDeviceParamsToOxygenMap.get(gceDriverOption).equals("-build_id")) {
199                     buildIdSet = true;
200                 }
201                 i++;
202             }
203             i++;
204         }
205 
206         // In case branch is set through gce-driver-param, but not build-id, set the name of
207         // branch to option `-build-id`, so LKGB will be used.
208         if (branch != null && !buildIdSet) {
209             oxygenClientArgs.add("-build_id");
210             oxygenClientArgs.add(branch);
211         }
212 
213         // check if build info exists after added from GceDriverParams
214         if (!oxygenClientArgs.contains("-build_target")) {
215             oxygenClientArgs.add("-build_target");
216             oxygenClientArgs.add(buildTarget);
217             oxygenClientArgs.add("-build_branch");
218             oxygenClientArgs.add(buildBranch);
219             oxygenClientArgs.add("-build_id");
220             oxygenClientArgs.add(buildId);
221         }
222 
223         // add oxygen side lease options
224         oxygenClientArgs.add("-target_region");
225         oxygenClientArgs.add(targetRegion);
226         oxygenClientArgs.add("-accounting_user");
227         oxygenClientArgs.add(accountingUser);
228         oxygenClientArgs.add("-lease_length_secs");
229         oxygenClientArgs.add(Long.toString(leaseLength / 1000));
230 
231         // Check if there is a new CVD path to override
232         if (extraOxygenArgs.containsKey("override_cvd_path")) {
233             oxygenClientArgs.add("-override_cvd_path");
234             oxygenClientArgs.add(extraOxygenArgs.get("override_cvd_path"));
235         }
236 
237         for (Map.Entry<String, String> arg : extraOxygenArgs.entrySet()) {
238             oxygenClientArgs.add("-" + arg.getKey());
239             if (!Strings.isNullOrEmpty(arg.getValue())) {
240                 oxygenClientArgs.add(arg.getValue());
241             }
242         }
243 
244         addInvocationAttributes(oxygenClientArgs, attributes);
245 
246         if (useOxygenation) {
247             oxygenClientArgs.add("-use_omnilab");
248         }
249 
250         CLog.i("Leasing device from oxygen client with %s", oxygenClientArgs.toString());
251         return runOxygenTimedCmd(
252                 oxygenClientArgs.toArray(new String[oxygenClientArgs.size()]), gceCmdTimeout);
253     }
254 
255     /**
256      * Attempt to lease multiple devices by calling Oxygen client binary.
257      *
258      * @param buildTargets a {@link List<String>} of build targets
259      * @param buildBranches a {@link List<String>} of build branches
260      * @param buildIds a {@link List<String>} of build IDs
261      * @param targetRegion target region for Oxygen instance
262      * @param accountingUser Oxygen accounting user email
263      * @param leaseLength number of ms for the lease duration
264      * @param gceDriverParams {@link List<String>} of gce driver params
265      * @param extraOxygenArgs {@link Map<String, String>} of extra Oxygen lease args
266      * @param attributes attributes associated with current invocation
267      * @param gceCmdTimeout number of ms for the command line timeout
268      * @param useOxygenation whether the device is leased from OmniLab Infra or not.
269      * @return {@link CommandResult} that Oxygen binary returned.
270      */
leaseMultipleDevices( List<String> buildTargets, List<String> buildBranches, List<String> buildIds, String targetRegion, String accountingUser, long leaseLength, Map<String, String> extraOxygenArgs, MultiMap<String, String> attributes, long gceCmdTimeout, boolean useOxygenation)271     public CommandResult leaseMultipleDevices(
272             List<String> buildTargets,
273             List<String> buildBranches,
274             List<String> buildIds,
275             String targetRegion,
276             String accountingUser,
277             long leaseLength,
278             Map<String, String> extraOxygenArgs,
279             MultiMap<String, String> attributes,
280             long gceCmdTimeout,
281             boolean useOxygenation) {
282         List<String> oxygenClientArgs = Lists.newArrayList(mCmdArgs);
283         oxygenClientArgs.add("-lease");
284 
285         if (buildTargets.size() > 0) {
286             oxygenClientArgs.add("-build_target");
287             oxygenClientArgs.add(String.join(",", buildTargets));
288         }
289 
290         if (buildBranches.size() > 0) {
291             oxygenClientArgs.add("-build_branch");
292             oxygenClientArgs.add(String.join(",", buildBranches));
293         }
294         if (buildIds.size() > 0) {
295             oxygenClientArgs.add("-build_id");
296             oxygenClientArgs.add(String.join(",", buildIds));
297         }
298         oxygenClientArgs.add("-multidevice_size");
299         oxygenClientArgs.add(String.valueOf(buildTargets.size()));
300         oxygenClientArgs.add("-target_region");
301         oxygenClientArgs.add(targetRegion);
302         oxygenClientArgs.add("-accounting_user");
303         oxygenClientArgs.add(accountingUser);
304         oxygenClientArgs.add("-lease_length_secs");
305         oxygenClientArgs.add(Long.toString(leaseLength / 1000));
306 
307         for (Map.Entry<String, String> arg : extraOxygenArgs.entrySet()) {
308             oxygenClientArgs.add("-" + arg.getKey());
309             if (!Strings.isNullOrEmpty(arg.getValue())) {
310                 oxygenClientArgs.add(arg.getValue());
311             }
312         }
313 
314         addInvocationAttributes(oxygenClientArgs, attributes);
315 
316         if (useOxygenation) {
317             oxygenClientArgs.add("-use_omnilab");
318         }
319 
320         CLog.i("Leasing multiple devices from oxygen client with %s", oxygenClientArgs.toString());
321         return runOxygenTimedCmd(
322                 oxygenClientArgs.toArray(new String[oxygenClientArgs.size()]), gceCmdTimeout);
323     }
324 
325     /**
326      * Attempt to release a device by using Oxygen client binary.
327      *
328      * @param instanceName name of the Oxygen instance
329      * @param host hostname of the Oxygen instance
330      * @param targetRegion target region
331      * @param accountingUser name of accounting user email
332      * @param extraOxygenArgs {@link Map<String, String>} of extra Oxygen args
333      * @param gceCmdTimeout number of ms for the command line timeout
334      * @param useOxygenation whether the device is leased from OmniLab Infra or not.
335      * @return a {@link CommandResult} that Oxygen binary returned.
336      */
release( String instanceName, String host, String targetRegion, String accountingUser, Map<String, String> extraOxygenArgs, long gceCmdTimeout, boolean useOxygenation)337     public CommandResult release(
338             String instanceName,
339             String host,
340             String targetRegion,
341             String accountingUser,
342             Map<String, String> extraOxygenArgs,
343             long gceCmdTimeout,
344             boolean useOxygenation) {
345         List<String> oxygenClientArgs = Lists.newArrayList(mCmdArgs);
346 
347         // If gceAvdInfo is missing info, then it means the device wasn't get leased successfully.
348         // In such case, there is no need to release the device.
349         if (instanceName == null || host == null) {
350             CommandResult res = new CommandResult();
351             res.setStatus(CommandStatus.SUCCESS);
352             return res;
353         }
354 
355         if (extraOxygenArgs != null) {
356             for (Map.Entry<String, String> arg : extraOxygenArgs.entrySet()) {
357                 oxygenClientArgs.add("-" + arg.getKey());
358                 if (!Strings.isNullOrEmpty(arg.getValue())) {
359                     oxygenClientArgs.add(arg.getValue());
360                 }
361             }
362         }
363 
364         oxygenClientArgs.add("-release");
365         oxygenClientArgs.add("-target_region");
366         oxygenClientArgs.add(targetRegion);
367         oxygenClientArgs.add("-server_url");
368         oxygenClientArgs.add(host);
369         oxygenClientArgs.add("-session_id");
370         oxygenClientArgs.add(instanceName);
371         oxygenClientArgs.add("-accounting_user");
372         oxygenClientArgs.add(accountingUser);
373         if (useOxygenation) {
374             oxygenClientArgs.add("-use_omnilab");
375         }
376         CLog.i("Releasing device from oxygen client with command %s", oxygenClientArgs.toString());
377         return runOxygenTimedCmd(
378                 oxygenClientArgs.toArray(new String[oxygenClientArgs.size()]), gceCmdTimeout);
379     }
380 
381     /**
382      * Create an adb or ssh tunnel to a given instance name and assign the endpoint to a device via
383      * LHP based on the given tunnel mode.
384      *
385      * @param mode The mode for oxygen client to talk to the device.
386      * @param portNumber The port number that Host Orchestrator communicates with.
387      * @param sessionId The session id returned by lease method in oxygenation.
388      * @param serverUrl The server url returned by lease method in oxygenation.
389      * @param targetRegion The target region for Oxygen instance
390      * @param accountingUser Oxygen accounting user email
391      * @param oxygenationDeviceId The device id returned by lease method in oxygenation.
392      * @param extraOxygenArgs {@link Map<String, String>} of extra Oxygen lease args
393      * @param tunnelLog {@link FileOutputStream} for storing logs.
394      * @return {@link Process} of the adb over LHP tunnel.
395      */
createTunnelViaLHP( LHPTunnelMode mode, String portNumber, String sessionId, String serverUrl, String targetRegion, String accountingUser, String oxygenationDeviceId, Map<String, String> extraOxygenArgs, FileOutputStream tunnelLog)396     public Process createTunnelViaLHP(
397             LHPTunnelMode mode,
398             String portNumber,
399             String sessionId,
400             String serverUrl,
401             String targetRegion,
402             String accountingUser,
403             String oxygenationDeviceId,
404             Map<String, String> extraOxygenArgs,
405             FileOutputStream tunnelLog) {
406         Process lhpTunnel = null;
407         List<String> oxygenClientArgs = Lists.newArrayList(mCmdArgs);
408         oxygenClientArgs.add("-build_lab_host_proxy_tunnel");
409         oxygenClientArgs.add("-server_url");
410         oxygenClientArgs.add(serverUrl);
411         oxygenClientArgs.add("-session_id");
412         oxygenClientArgs.add(sessionId);
413 
414         if (extraOxygenArgs != null) {
415             for (Map.Entry<String, String> arg : extraOxygenArgs.entrySet()) {
416                 oxygenClientArgs.add("-" + arg.getKey());
417                 if (!Strings.isNullOrEmpty(arg.getValue())) {
418                     oxygenClientArgs.add(arg.getValue());
419                 }
420             }
421         }
422 
423         oxygenClientArgs.add("-target_region");
424         oxygenClientArgs.add(targetRegion);
425         oxygenClientArgs.add("-accounting_user");
426         oxygenClientArgs.add(accountingUser);
427         oxygenClientArgs.add("-use_omnilab");
428         oxygenClientArgs.add("-tunnel_type");
429         if (LHPTunnelMode.ADB.equals(mode)) {
430             oxygenClientArgs.add("adb");
431         } else if (LHPTunnelMode.CURL.equals(mode)) {
432             oxygenClientArgs.add("curl");
433         } else {
434             oxygenClientArgs.add("ssh");
435         }
436         oxygenClientArgs.add("-tunnel_local_port");
437         oxygenClientArgs.add(portNumber);
438         oxygenClientArgs.add("-device_id");
439         oxygenClientArgs.add(oxygenationDeviceId);
440         try {
441             DateFormat dateFormat = new SimpleDateFormat("MM/dd/yy HH:mm:SS");
442             CLog.i(
443                     "Building %s tunnel from oxygen client with command %s...",
444                     mode, oxygenClientArgs.toString());
445             tunnelLog.write(
446                     String.format(
447                                     "\n===[%s]Session id: %s, Server URL: %s, Port: %s===\n",
448                                     dateFormat.format(System.currentTimeMillis()),
449                                     sessionId,
450                                     serverUrl,
451                                     portNumber)
452                             .getBytes());
453             lhpTunnel = getRunUtil().runCmdInBackground(oxygenClientArgs, tunnelLog);
454             // TODO(b/363861223): reduce the waiting time when LHP is stable.
455             getRunUtil().sleep(30 * 1000);
456         } catch (IOException e) {
457             CLog.d("Failed connecting to remote GCE using %s over LHP, %s", mode, e.getMessage());
458         }
459         if (lhpTunnel == null || !lhpTunnel.isAlive()) {
460             closeLHPConnection(lhpTunnel);
461             InvocationMetricLogger.addInvocationMetrics(
462                     InvocationMetricLogger.InvocationMetricKey.PORTFORWARD_LHP_FAIL_COUNT, 1);
463             return null;
464         }
465         InvocationMetricLogger.addInvocationMetrics(
466                 InvocationMetricLogger.InvocationMetricKey.PORTFORWARD_LHP_SUCCESS_COUNT, 1);
467         return lhpTunnel;
468     }
469 
470     /** Helper to create an unused server socket. */
createServerSocket()471     public Integer createServerSocket() {
472         ServerSocket s = null;
473         try {
474             s = new ServerSocket(0);
475             // even after being closed, socket may remain in TIME_WAIT state
476             // reuse address allows to connect to it even in this state.
477             s.setReuseAddress(true);
478             s.close();
479         } catch (IOException e) {
480             CLog.d("Failed to connect to remote GCE using adb tunnel %s", e.getMessage());
481         }
482         return s.getLocalPort();
483     }
484 
485     /** Close the connection to the remote oxygenation device with a given {@link Process}. */
closeLHPConnection(Process p)486     public void closeLHPConnection(Process p) {
487         if (p != null) {
488             p.destroy();
489             try {
490                 boolean res = p.waitFor(20 * 1000, TimeUnit.MILLISECONDS);
491                 if (!res) {
492                     CLog.e("Tunnel may not have properly terminated.");
493                 }
494             } catch (InterruptedException e) {
495                 CLog.e("Tunnel interrupted during shutdown: %s", e.getMessage());
496             }
497         }
498     }
499 
500     /**
501      * Utility function to execute a timed Oxygen command with logging.
502      *
503      * @param oxygenCmd command line options.
504      * @param timeout command timeout.
505      * @return {@link CommandResult}.
506      */
runOxygenTimedCmd(String[] oxygenCmd, long timeout)507     private CommandResult runOxygenTimedCmd(String[] oxygenCmd, long timeout) {
508         CommandResult res = getRunUtil().runTimedCmd(timeout, oxygenCmd);
509         CLog.i(
510                 "Oxygen client result status: %s, stdout: %s, stderr: %s",
511                 res.getStatus(), res.getStdout(), res.getStderr());
512         return res;
513     }
514 }
515