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