• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.tradefed.device.TestDeviceOptions;
19 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
20 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
21 import com.android.tradefed.invoker.tracing.CloseableTraceScope;
22 import com.android.tradefed.log.ITestLogger;
23 import com.android.tradefed.log.LogUtil.CLog;
24 import com.android.tradefed.result.FileInputStreamSource;
25 import com.android.tradefed.result.LogDataType;
26 import com.android.tradefed.targetprep.TargetSetupError;
27 import com.android.tradefed.util.FileUtil;
28 import com.android.tradefed.util.GCSFileDownloader;
29 import com.android.tradefed.util.SystemUtil;
30 import com.android.tradefed.util.avd.LogCollector;
31 import com.android.tradefed.util.avd.OxygenClient;
32 
33 import com.google.common.annotations.VisibleForTesting;
34 import com.google.common.base.Strings;
35 
36 import java.io.BufferedReader;
37 import java.io.File;
38 import java.io.InputStreamReader;
39 import java.net.HttpURLConnection;
40 import java.net.URL;
41 import java.util.AbstractMap;
42 import java.util.Arrays;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Set;
46 import java.util.regex.Matcher;
47 import java.util.regex.Pattern;
48 import java.util.stream.Collectors;
49 import java.util.stream.Stream;
50 
51 /** Utility to interact with Oxygen service. */
52 public class OxygenUtil {
53 
54     // URL for retrieving instance metadata related to the computing zone.
55     private static final String ZONE_METADATA_URL =
56             "http://metadata/computeMetadata/v1/instance/zone";
57 
58     // Default region if no specific zone is provided.
59     private static final String DEFAULT_REGION = "us-west1";
60 
61     private GCSFileDownloader mDownloader;
62 
63     // TODO: Support more type of log data types
64     private static final Map<Pattern, LogDataType> REMOTE_LOG_NAME_PATTERN_TO_TYPE_MAP =
65             Stream.of(
66                             new AbstractMap.SimpleEntry<>(
67                                     Pattern.compile("^logcat.*"), LogDataType.LOGCAT),
68                             new AbstractMap.SimpleEntry<>(
69                                     Pattern.compile(".*kernel.*"), LogDataType.KERNEL_LOG),
70                             new AbstractMap.SimpleEntry<>(
71                                     Pattern.compile(".*bugreport.*zip"), LogDataType.BUGREPORTZ),
72                             new AbstractMap.SimpleEntry<>(
73                                     Pattern.compile(".*bugreport.*txt"), LogDataType.BUGREPORT),
74                             new AbstractMap.SimpleEntry<>(
75                                     Pattern.compile(".*tombstones-zip.*zip"),
76                                     LogDataType.TOMBSTONEZ))
77                     .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
78 
79 
80     /** Default constructor of OxygenUtil */
OxygenUtil()81     public OxygenUtil() {
82         mDownloader = new GCSFileDownloader(true);
83     }
84 
85     /**
86      * Constructor of OxygenUtil
87      *
88      * @param downloader {@link GCSFileDownloader} to download file from GCS.
89      */
90     @VisibleForTesting
OxygenUtil(GCSFileDownloader downloader)91     OxygenUtil(GCSFileDownloader downloader) {
92         mDownloader = downloader;
93     }
94 
95     /**
96      * Download error logs from GCS when Oxygen failed to launch a virtual device.
97      *
98      * @param error TargetSetupError raised when leasing device through Oxygen service.
99      * @param logger The {@link ITestLogger} where to log the file
100      */
downloadLaunchFailureLogs(TargetSetupError error, ITestLogger logger)101     public void downloadLaunchFailureLogs(TargetSetupError error, ITestLogger logger) {
102         String errorMessage = error.getMessage();
103         if (error.getCause() != null) {
104             // Also include the message from the internal cause.
105             errorMessage = String.format("%s %s", errorMessage, error.getCause().getMessage());
106         }
107 
108         File localDir = LogCollector.downloadLaunchFailureLogs(errorMessage, mDownloader);
109         if (localDir == null) {
110             return;
111         }
112         try {
113             String oxygenVersion = LogCollector.collectOxygenVersion(localDir);
114             if (!Strings.isNullOrEmpty(oxygenVersion)) {
115                 InvocationMetricLogger.addInvocationMetrics(
116                         InvocationMetricLogger.InvocationMetricKey.CF_OXYGEN_VERSION,
117                         oxygenVersion);
118             }
119             try (CloseableTraceScope ignore =
120                     new CloseableTraceScope("avd:collectErrorSignature")) {
121                 List<String> signatures = LogCollector.collectErrorSignatures(localDir);
122                 if (signatures.size() > 0) {
123                     InvocationMetricLogger.addInvocationMetrics(
124                             InvocationMetricKey.DEVICE_ERROR_SIGNATURES,
125                             String.join(",", signatures));
126                 }
127             }
128             Set<String> files = FileUtil.findFiles(localDir, ".*");
129             for (String f : files) {
130                 File file = new File(f);
131                 if (file.isDirectory()) {
132                     continue;
133                 }
134                 CLog.d("Logging %s", f);
135                 try (FileInputStreamSource data = new FileInputStreamSource(file)) {
136                     String logFileName =
137                             "oxygen_"
138                                     + localDir.toPath()
139                                             .relativize(file.toPath())
140                                             .toString()
141                                             .replace(File.separatorChar, '_');
142                     LogDataType logDataType = getDefaultLogType(logFileName);
143                     if (logDataType == LogDataType.UNKNOWN) {
144                         // Default log type to be CUTTLEFISH_LOG to avoid compression.
145                         logDataType = LogDataType.CUTTLEFISH_LOG;
146                     }
147                     logger.testLog(logFileName, logDataType, data);
148                 }
149             }
150         } catch (Exception e) {
151             CLog.e("Failed to parse Oxygen log from %s", localDir);
152             CLog.e(e);
153         }
154     }
155 
156     /**
157      * Determine a log file's log data type based on its name.
158      *
159      * @param logFileName The remote log file's name.
160      * @return A {@link LogDataType} which the log file associates with. Will return the type
161      *     UNKNOWN if unable to determine the log data type based on its name.
162      */
getDefaultLogType(String logFileName)163     public static LogDataType getDefaultLogType(String logFileName) {
164         for (Map.Entry<Pattern, LogDataType> entry :
165                 REMOTE_LOG_NAME_PATTERN_TO_TYPE_MAP.entrySet()) {
166             Matcher matcher = entry.getKey().matcher(logFileName);
167             if (matcher.find()) {
168                 return entry.getValue();
169             }
170         }
171         CLog.d(
172                 String.format(
173                         "Unable to determine log type of the remote log file %s, log type is"
174                                 + " UNKNOWN",
175                         logFileName));
176         return LogDataType.UNKNOWN;
177     }
178 
179 
180     /**
181      * Retrieves the target region based on the provided device options. If the target region is
182      * explicitly set in the device options, it returns the specified region. If the target region
183      * is not set, it retrieves the region based on the instance's zone.
184      *
185      * @param deviceOptions The TestDeviceOptions object containing device options.
186      * @return The target region.
187      */
getTargetRegion(TestDeviceOptions deviceOptions)188     public static String getTargetRegion(TestDeviceOptions deviceOptions) {
189         if (deviceOptions.getOxygenTargetRegion() != null) {
190             return deviceOptions.getOxygenTargetRegion();
191         }
192         try {
193             URL url = new URL(ZONE_METADATA_URL);
194             HttpURLConnection connection = (HttpURLConnection) url.openConnection();
195             connection.setRequestProperty("Metadata-Flavor", "Google");
196 
197             StringBuilder response = new StringBuilder();
198             try (BufferedReader reader =
199                     new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
200                 String line;
201                 while ((line = reader.readLine()) != null) {
202                     response.append(line);
203                 }
204             }
205 
206             return getRegionFromZoneMeta(response.toString());
207         } catch (Exception e) {
208             // Error occurred while fetching zone information, fallback to default region.
209             CLog.e(e);
210             return DEFAULT_REGION;
211         }
212     }
213 
214     /**
215      * Retrieves the region from a given zone string.
216      *
217      * @param zone The input zone string in the format "projects/12345/zones/us-west12-a".
218      * @return The extracted region string, e.g., "us-west12".
219      */
getRegionFromZoneMeta(String zone)220     public static String getRegionFromZoneMeta(String zone) {
221         int lastSlashIndex = zone.lastIndexOf("/");
222         String region = zone.substring(lastSlashIndex + 1);
223         int lastDashIndex = region.lastIndexOf("-");
224         return region.substring(0, lastDashIndex);
225     }
226 
227     /**
228      * Helper to create an {@link OxygenClient}.
229      *
230      * @param file the Oxygen client binary file.
231      * @return an {@link OxygenClient} class to create CF devices.
232      */
createOxygenClient(File file)233     public static OxygenClient createOxygenClient(File file) {
234         if (file.getAbsolutePath().endsWith(".jar")) {
235             List<String> cmdArgs =
236                     Arrays.asList(
237                             SystemUtil.getRunningJavaBinaryPath().getAbsolutePath(),
238                             "-Xmx256m",
239                             "-XX:G1HeapWastePercent=5",
240                             "-jar",
241                             file.getAbsolutePath());
242             return new OxygenClient(cmdArgs);
243         }
244         return new OxygenClient(Arrays.asList(file.getAbsolutePath()));
245     }
246 }
247