• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.command.remote.DeviceDescriptor;
19 import com.android.tradefed.device.TestDeviceOptions;
20 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
21 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
22 import com.android.tradefed.log.LogUtil.CLog;
23 import com.android.tradefed.result.LogDataType;
24 import com.android.tradefed.result.error.ErrorIdentifier;
25 import com.android.tradefed.result.error.InfraErrorIdentifier;
26 import com.android.tradefed.targetprep.TargetSetupError;
27 import com.android.tradefed.util.CommandResult;
28 import com.android.tradefed.util.CommandStatus;
29 
30 import com.google.common.annotations.VisibleForTesting;
31 import com.google.common.base.Strings;
32 import com.google.common.collect.ImmutableMap;
33 import com.google.common.net.HostAndPort;
34 
35 import org.json.JSONArray;
36 import org.json.JSONException;
37 import org.json.JSONObject;
38 
39 import java.io.File;
40 import java.io.IOException;
41 import java.nio.charset.StandardCharsets;
42 import java.nio.file.Files;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.HashMap;
46 import java.util.LinkedHashMap;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.regex.Matcher;
50 import java.util.regex.Pattern;
51 
52 /** Structure to hold relevant data for a given GCE AVD instance. */
53 public class GceAvdInfo {
54 
55     // Patterns to match from Oxygen client's return message to identify error.
56     private static final LinkedHashMap<InfraErrorIdentifier, String> OXYGEN_ERROR_PATTERN_MAP;
57 
58     private static final Pattern OXYGEN_LEASE_RESPONSE_PATTERN =
59             Pattern.compile(
60                     "session_id:\\s?\"(.*?)\".*?server_url:\\s?\"(.*?)\".*?oxygen_version:\\s?\"(.*?)\"",
61                     Pattern.DOTALL);
62     private static final Pattern OXYGENATION_LEASE_RESPONSE_PATTERN =
63             Pattern.compile(
64                     "session_id:\\s?\"(.*?)\".*?server_url:\\s?\"(.*?)\".*?oxygen_version:\\s?\"(.*?)\".*?device_id:\\s?\"(.*?)\"",
65                     Pattern.DOTALL);
66 
67     static {
68         OXYGEN_ERROR_PATTERN_MAP = new LinkedHashMap<InfraErrorIdentifier, String>();
69         // Order the error message matching carefully so it can surface the expected error properly
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_SERVER_SHUTTING_DOWN, "server_shutting_down")70         OXYGEN_ERROR_PATTERN_MAP.put(
71                 InfraErrorIdentifier.OXYGEN_SERVER_SHUTTING_DOWN, "server_shutting_down");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_BAD_GATEWAY_ERROR, "UNAVAILABLE: HTTP status code 502")72         OXYGEN_ERROR_PATTERN_MAP.put(
73                 InfraErrorIdentifier.OXYGEN_BAD_GATEWAY_ERROR, "UNAVAILABLE: HTTP status code 502");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_REQUEST_TIMEOUT, "DeadlineExceeded")74         OXYGEN_ERROR_PATTERN_MAP.put(
75                 InfraErrorIdentifier.OXYGEN_REQUEST_TIMEOUT, "DeadlineExceeded");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_RESOURCE_EXHAUSTED, "ResourceExhausted")76         OXYGEN_ERROR_PATTERN_MAP.put(
77                 InfraErrorIdentifier.OXYGEN_RESOURCE_EXHAUSTED, "ResourceExhausted");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_NOT_ENOUGH_RESOURCE, "Oxygen currently doesn't have enough resources to fulfil this request")78         OXYGEN_ERROR_PATTERN_MAP.put(
79                 InfraErrorIdentifier.OXYGEN_NOT_ENOUGH_RESOURCE,
80                 "Oxygen currently doesn't have enough resources to fulfil this request");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_SERVER_CONNECTION_FAILURE, "Bad Gateway")81         OXYGEN_ERROR_PATTERN_MAP.put(
82                 InfraErrorIdentifier.OXYGEN_SERVER_CONNECTION_FAILURE, "Bad Gateway");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_CLIENT_LEASE_ERROR, "OxygenClient")83         OXYGEN_ERROR_PATTERN_MAP.put(
84                 InfraErrorIdentifier.OXYGEN_CLIENT_LEASE_ERROR, "OxygenClient");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_DEVICE_LAUNCHER_TIMEOUT, "Lease aborted due to launcher failure: Timed out waiting for virtual device to" + " start")85         OXYGEN_ERROR_PATTERN_MAP.put(
86                 InfraErrorIdentifier.OXYGEN_DEVICE_LAUNCHER_TIMEOUT,
87                 "Lease aborted due to launcher failure: Timed out waiting for virtual device to"
88                         + " start");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_DEVICE_LAUNCHER_FAILURE, "Lease aborted due to launcher failure")89         OXYGEN_ERROR_PATTERN_MAP.put(
90                 InfraErrorIdentifier.OXYGEN_DEVICE_LAUNCHER_FAILURE,
91                 "Lease aborted due to launcher failure");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_SERVER_LB_CONNECTION_ERROR, "desc = connection error")92         OXYGEN_ERROR_PATTERN_MAP.put(
93                 InfraErrorIdentifier.OXYGEN_SERVER_LB_CONNECTION_ERROR, "desc = connection error");
94     }
95 
96     // Error message for specify Oxygen error.
97     private static final ImmutableMap<InfraErrorIdentifier, String> OXYGEN_ERROR_MESSAGE_MAP =
98             ImmutableMap.of(
99                     InfraErrorIdentifier.OXYGEN_DEVICE_LAUNCHER_FAILURE,
100                             "AVD failed to boot up properly",
101                     InfraErrorIdentifier.OXYGEN_SERVER_SHUTTING_DOWN,
102                             "Unexpected error from Oxygen service",
103                     InfraErrorIdentifier.OXYGEN_BAD_GATEWAY_ERROR,
104                             "Unexpected error from Oxygen service",
105                     InfraErrorIdentifier.OXYGEN_REQUEST_TIMEOUT,
106                             "Unexpected error from Oxygen service. Request timed out.",
107                     InfraErrorIdentifier.OXYGEN_RESOURCE_EXHAUSTED,
108                             "Oxygen ran out of capacity to lease virtual device",
109                     InfraErrorIdentifier.OXYGEN_SERVER_CONNECTION_FAILURE,
110                             "Unexpected error from Oxygen service",
111                     InfraErrorIdentifier.OXYGEN_CLIENT_LEASE_ERROR,
112                             "Oxygen client failed to lease a device",
113                     InfraErrorIdentifier.OXYGEN_DEVICE_LAUNCHER_TIMEOUT, "AVD boot timed out");
114 
115     public static class LogFileEntry {
116         public final String path;
117         public final LogDataType type;
118         // The name is optional and defaults to an empty string.
119         public final String name;
120 
121         @VisibleForTesting
LogFileEntry(String path, LogDataType type, String name)122         LogFileEntry(String path, LogDataType type, String name) {
123             this.path = path;
124             this.type = type;
125             this.name = name;
126         }
127 
LogFileEntry(JSONObject log)128         LogFileEntry(JSONObject log) throws JSONException {
129             path = log.getString("path");
130             type = parseLogDataType(log.getString("type"));
131             name = log.optString("name", "");
132         }
133 
parseLogDataType(String typeString)134         private LogDataType parseLogDataType(String typeString) {
135             try {
136                 return LogDataType.valueOf(typeString);
137             } catch (IllegalArgumentException e) {
138                 CLog.w("Unknown log type in GCE AVD info: %s", typeString);
139                 return LogDataType.UNKNOWN;
140             }
141         }
142     }
143 
144     public static final List<String> BUILD_VARS =
145             Arrays.asList(
146                     "build_id",
147                     "build_target",
148                     "branch",
149                     "kernel_build_id",
150                     "kernel_build_target",
151                     "kernel_branch",
152                     "system_build_id",
153                     "system_build_target",
154                     "system_branch",
155                     "emulator_build_id",
156                     "emulator_build_target",
157                     "emulator_branch");
158 
159     private String mInstanceName;
160     private HostAndPort mHostAndPort;
161     private ErrorIdentifier mErrorType;
162     private String mErrors;
163     private GceStatus mStatus;
164     private HashMap<String, String> mBuildVars;
165     private List<LogFileEntry> mLogs;
166     private boolean mIsIpPreconfigured = false;
167     private Integer mDeviceOffset = null;
168     private String mInstanceUser = null;
169     // Skip collecting device log if set to true.
170     private boolean mSkipDeviceLogCollection = false;
171     private String mOxygenationDeviceId = null;
172 
173     public static enum GceStatus {
174         SUCCESS,
175         FAIL,
176         BOOT_FAIL,
177         DEVICE_OFFLINE,
178     }
179 
GceAvdInfo(String instanceName, HostAndPort hostAndPort)180     public GceAvdInfo(String instanceName, HostAndPort hostAndPort) {
181         mInstanceName = instanceName;
182         mHostAndPort = hostAndPort;
183         mBuildVars = new HashMap<String, String>();
184         mLogs = new ArrayList<LogFileEntry>();
185     }
186 
GceAvdInfo( String instanceName, HostAndPort hostAndPort, ErrorIdentifier errorType, String errors, GceStatus status)187     public GceAvdInfo(
188             String instanceName,
189             HostAndPort hostAndPort,
190             ErrorIdentifier errorType,
191             String errors,
192             GceStatus status) {
193         this(instanceName, hostAndPort);
194         mErrorType = errorType;
195         mErrors = errors;
196         mStatus = status;
197     }
198 
199     /** {@inheritDoc} */
200     @Override
toString()201     public String toString() {
202         return "GceAvdInfo [mInstanceName="
203                 + mInstanceName
204                 + ", mHostAndPort="
205                 + mHostAndPort
206                 + ", mDeviceOffset="
207                 + mDeviceOffset
208                 + ", mInstanceUser="
209                 + mInstanceUser
210                 + ", mErrorType="
211                 + mErrorType
212                 + ", mErrors="
213                 + mErrors
214                 + ", mStatus="
215                 + mStatus
216                 + ", mIsIpPreconfigured="
217                 + mIsIpPreconfigured
218                 + ", mBuildVars="
219                 + ", mOxygenationDeviceId="
220                 + mOxygenationDeviceId
221                 + mBuildVars.toString()
222                 + ", mLogs="
223                 + mLogs.toString()
224                 + "]";
225     }
226 
getOxygenationDeviceId()227     public String getOxygenationDeviceId() {
228         return mOxygenationDeviceId;
229     }
230 
setOxygenationDeviceId(String deviceId)231     public void setOxygenationDeviceId(String deviceId) {
232         mOxygenationDeviceId = deviceId;
233     }
234 
instanceName()235     public String instanceName() {
236         return mInstanceName;
237     }
238 
hostAndPort()239     public HostAndPort hostAndPort() {
240         return mHostAndPort;
241     }
242 
getErrorType()243     public ErrorIdentifier getErrorType() {
244         return mErrorType;
245     }
246 
setErrorType(ErrorIdentifier errorType)247     public void setErrorType(ErrorIdentifier errorType) {
248         mErrorType = errorType;
249     }
250 
getErrors()251     public String getErrors() {
252         return mErrors;
253     }
254 
setErrors(String errors)255     public void setErrors(String errors) {
256         mErrors = errors;
257     }
258 
259     /** Return the map from local or remote log paths to types. */
getLogs()260     public List<LogFileEntry> getLogs() {
261         return mLogs;
262     }
263 
getStatus()264     public GceStatus getStatus() {
265         return mStatus;
266     }
267 
setStatus(GceStatus status)268     public void setStatus(GceStatus status) {
269         mStatus = status;
270     }
271 
addBuildVar(String buildKey, String buildValue)272     private void addBuildVar(String buildKey, String buildValue) {
273         mBuildVars.put(buildKey, buildValue);
274     }
275 
setIpPreconfigured(boolean isIpPreconfigured)276     public void setIpPreconfigured(boolean isIpPreconfigured) {
277         mIsIpPreconfigured = isIpPreconfigured;
278     }
279 
isIpPreconfigured()280     public boolean isIpPreconfigured() {
281         return mIsIpPreconfigured;
282     }
283 
setDeviceOffset(Integer deviceOffset)284     public void setDeviceOffset(Integer deviceOffset) {
285         mDeviceOffset = deviceOffset;
286     }
287 
getDeviceOffset()288     public Integer getDeviceOffset() {
289         return mDeviceOffset;
290     }
291 
setInstanceUser(String instanceUser)292     public void setInstanceUser(String instanceUser) {
293         mInstanceUser = instanceUser;
294     }
295 
getInstanceUser()296     public String getInstanceUser() {
297         return mInstanceUser;
298     }
299 
300     /**
301      * Return build variable information hash of GCE AVD device.
302      *
303      * <p>Possible build variables keys are described in BUILD_VARS for example: build_id,
304      * build_target, branch, kernel_build_id, kernel_build_target, kernel_branch, system_build_id,
305      * system_build_target, system_branch, emulator_build_id, emulator_build_target,
306      * emulator_branch.
307      */
getBuildVars()308     public HashMap<String, String> getBuildVars() {
309         return new HashMap<String, String>(mBuildVars);
310     }
311 
getSkipDeviceLogCollection()312     public boolean getSkipDeviceLogCollection() {
313         return mSkipDeviceLogCollection;
314     }
315 
316     // TODO(b/329150949): Remove after lab update
setSkipBugreportCollection(boolean skipDeviceLogCollection)317     public void setSkipBugreportCollection(boolean skipDeviceLogCollection) {
318         mSkipDeviceLogCollection = skipDeviceLogCollection;
319     }
320 
setSkipDeviceLogCollection(boolean skipDeviceLogCollection)321     public void setSkipDeviceLogCollection(boolean skipDeviceLogCollection) {
322         mSkipDeviceLogCollection = skipDeviceLogCollection;
323     }
324 
325     /**
326      * Parse a given file to obtain the GCE AVD device info.
327      *
328      * @param f {@link File} file to read the JSON output from GCE Driver.
329      * @param descriptor the descriptor of the device that needs the info.
330      * @param remoteAdbPort the remote port that should be used for adb connection
331      * @return the {@link GceAvdInfo} of the device if found, or null if error.
332      */
parseGceInfoFromFile( File f, DeviceDescriptor descriptor, int remoteAdbPort)333     public static GceAvdInfo parseGceInfoFromFile(
334             File f, DeviceDescriptor descriptor, int remoteAdbPort) throws TargetSetupError {
335         String data;
336         try {
337           data = Files.readString(f.toPath(), StandardCharsets.UTF_8);
338         } catch (IOException e) {
339             CLog.e("Failed to read result file from GCE driver:");
340             CLog.e(e);
341             return null;
342         }
343         return parseGceInfoFromString(data, descriptor, remoteAdbPort);
344     }
345 
346     /**
347      * Parse a given string to obtain the GCE AVD device info.
348      *
349      * @param data JSON string.
350      * @param descriptor the descriptor of the device that needs the info.
351      * @param remoteAdbPort the remote port that should be used for adb connection
352      * @return the {@link GceAvdInfo} of the device if found, or null if error.
353      */
parseGceInfoFromString( String data, DeviceDescriptor descriptor, int remoteAdbPort)354     public static GceAvdInfo parseGceInfoFromString(
355             String data, DeviceDescriptor descriptor, int remoteAdbPort) throws TargetSetupError {
356         if (Strings.isNullOrEmpty(data)) {
357             CLog.w("No data provided");
358             return null;
359         }
360         InfraErrorIdentifier errorId = null;
361         String errors = data;
362         try {
363             errors = parseErrorField(data);
364             JSONObject res = new JSONObject(data);
365             String status = res.getString("status");
366             GceStatus gceStatus = GceStatus.valueOf(status);
367             String errorType = res.has("error_type") ? res.getString("error_type") : null;
368             if (errorType == null) {
369                 // Parse more detailed error type if we can.
370                 if (errors.contains("QUOTA_EXCEED") && errors.contains("GPU")) {
371                     errorType = "ACLOUD_QUOTA_EXCEED_GPU";
372                 }
373             }
374             errorId =
375                     GceStatus.SUCCESS.equals(gceStatus)
376                             ? null
377                             : determineAcloudErrorType(errorType);
378             if (errorId == InfraErrorIdentifier.ACLOUD_OXYGEN_LEASE_ERROR) {
379                 errorId = refineOxygenErrorType(errors);
380             }
381             JSONArray devices = null;
382             if (GceStatus.FAIL.equals(gceStatus) || GceStatus.BOOT_FAIL.equals(gceStatus)) {
383                 // In case of failure we still look for instance name to shutdown if needed.
384                 if (res.getJSONObject("data").has("devices_failing_boot")) {
385                     devices = res.getJSONObject("data").getJSONArray("devices_failing_boot");
386                 }
387             } else {
388                 devices = res.getJSONObject("data").getJSONArray("devices");
389             }
390             if (devices != null) {
391                 if (devices.length() == 1) {
392                     JSONObject d = (JSONObject) devices.get(0);
393                     // Only log CF boot performance metrics if the device launch is successful.
394                     if (GceStatus.SUCCESS.equals(gceStatus)) {
395                         addCfStartTimeMetrics(d);
396                     }
397                     String ip = d.getString("ip");
398                     String instanceName = d.getString("instance_name");
399                     GceAvdInfo avdInfo =
400                             new GceAvdInfo(
401                                     instanceName,
402                                     HostAndPort.fromString(ip).withDefaultPort(remoteAdbPort),
403                                     errorId,
404                                     errors,
405                                     gceStatus);
406                     avdInfo.mLogs.addAll(parseLogField(d));
407                     for (String buildVar : BUILD_VARS) {
408                         if (d.has(buildVar) && !d.getString(buildVar).trim().isEmpty()) {
409                             avdInfo.addBuildVar(buildVar, d.getString(buildVar).trim());
410                         }
411                     }
412                     return avdInfo;
413                 } else {
414                     CLog.w("Expected only one device to return but found %d", devices.length());
415                 }
416             } else {
417                 CLog.w("No device information, device was not started.");
418             }
419         } catch (JSONException e) {
420             CLog.e("Failed to parse JSON %s:", data);
421             CLog.e(e);
422         }
423 
424         // If errors are found throw an exception with the acloud message.
425         if (errorId == null) {
426             errorId = InfraErrorIdentifier.ACLOUD_UNDETERMINED;
427         }
428         throw new TargetSetupError(
429                 String.format("acloud errors: %s", !errors.isEmpty() ? errors : data),
430                 descriptor,
431                 errorId);
432     }
433 
434     /**
435      * Parse a given command line output from Oxygen client binary to obtain leased AVD info.
436      *
437      * @param oxygenRes the {@link CommandResult} from Oxygen client command execution.
438      * @param deviceOptions the {@link TestDeviceOptions} describing the device options
439      * @return {@link List} of the devices successfully leased. Will throw {@link TargetSetupError}
440      *     if failed to lease a device.
441      */
parseGceInfoFromOxygenClientOutput( CommandResult oxygenRes, TestDeviceOptions deviceOptions)442     public static List<GceAvdInfo> parseGceInfoFromOxygenClientOutput(
443             CommandResult oxygenRes, TestDeviceOptions deviceOptions) throws TargetSetupError {
444         CommandStatus oxygenCliStatus = oxygenRes.getStatus();
445         if (CommandStatus.SUCCESS.equals(oxygenCliStatus)) {
446             return parseSucceedOxygenClientOutput(
447                     oxygenRes.getStdout() + oxygenRes.getStderr(), deviceOptions);
448         } else if (CommandStatus.TIMED_OUT.equals(oxygenCliStatus)) {
449             return Arrays.asList(
450                     new GceAvdInfo(
451                             null,
452                             null,
453                             InfraErrorIdentifier.OXYGEN_CLIENT_BINARY_TIMEOUT,
454                             "Oxygen client binary CLI timed out",
455                             GceStatus.FAIL));
456         } else {
457             CLog.d(
458                     "OxygenClient - CommandStatus: %s, output: %s\", output",
459                     oxygenCliStatus, oxygenRes.getStdout() + " " + oxygenRes.getStderr());
460             InfraErrorIdentifier identifier = refineOxygenErrorType(oxygenRes.getStderr());
461             throw new TargetSetupError(
462                     OXYGEN_ERROR_MESSAGE_MAP.getOrDefault(
463                             identifier, "Oxygen client failed to lease a device"),
464                     new Exception(
465                             oxygenRes.getStderr()), // Include the original error message as cause.
466                     identifier);
467         }
468     }
469 
parseSucceedOxygenClientOutput( String output, TestDeviceOptions deviceOptions)470     private static List<GceAvdInfo> parseSucceedOxygenClientOutput(
471             String output, TestDeviceOptions deviceOptions) throws TargetSetupError {
472         CLog.d("Parsing oxygen client output: %s", output);
473         List<GceAvdInfo> gceAvdInfos = new ArrayList<>();
474         if (deviceOptions.useOxygenationDevice()) {
475             Matcher matcher = OXYGENATION_LEASE_RESPONSE_PATTERN.matcher(output);
476             while (matcher.find()) {
477                 String sessionId = matcher.group(1);
478                 String serverUrl = matcher.group(2);
479                 String oxygenationVersion = matcher.group(3);
480                 String deviceId = matcher.group(4);
481                 GceAvdInfo gceAvdInfo =
482                         new GceAvdInfo(
483                                 sessionId,
484                                 HostAndPort.fromString(serverUrl),
485                                 null,
486                                 null,
487                                 GceStatus.SUCCESS);
488                 gceAvdInfo.setOxygenationDeviceId(deviceId);
489                 gceAvdInfos.add(gceAvdInfo);
490                 InvocationMetricLogger.addInvocationMetrics(
491                         InvocationMetricKey.CF_OXYGENATION_VERSION, oxygenationVersion);
492             }
493         } else {
494             Matcher matcher = OXYGEN_LEASE_RESPONSE_PATTERN.matcher(output);
495             int deviceOffset = 0;
496             while (matcher.find()) {
497                 String sessionId = matcher.group(1);
498                 String serverUrl = matcher.group(2);
499                 String oxygenVersion = matcher.group(3);
500                 gceAvdInfos.add(
501                         new GceAvdInfo(
502                                 sessionId,
503                                 HostAndPort.fromString(serverUrl)
504                                         .withDefaultPort(
505                                                 deviceOptions.getRemoteAdbPort() + deviceOffset),
506                                 null,
507                                 null,
508                                 GceStatus.SUCCESS));
509                 InvocationMetricLogger.addInvocationMetrics(
510                         InvocationMetricKey.CF_OXYGEN_VERSION, oxygenVersion);
511                 deviceOffset++;
512             }
513         }
514         if (gceAvdInfos.isEmpty()) {
515             throw new TargetSetupError(
516                     String.format("Failed to parse the output: %s", output),
517                     InfraErrorIdentifier.OXYGEN_CLIENT_BINARY_ERROR);
518         }
519 
520         return gceAvdInfos;
521     }
522 
523     /**
524      * Search error message from Oxygen service for more accurate error code.
525      *
526      * @param errors error messages returned by Oxygen service.
527      * @return InfraErrorIdentifier for the Oxygen service error.
528      */
529     @VisibleForTesting
refineOxygenErrorType(String errors)530     static InfraErrorIdentifier refineOxygenErrorType(String errors) {
531         for (Map.Entry<InfraErrorIdentifier, String> entry : OXYGEN_ERROR_PATTERN_MAP.entrySet()) {
532             if (errors.contains(entry.getValue()))
533                 return entry.getKey();
534          }
535 
536         return InfraErrorIdentifier.ACLOUD_OXYGEN_LEASE_ERROR;
537     }
538 
parseErrorField(String data)539     private static String parseErrorField(String data) throws JSONException {
540         String res = "";
541         JSONObject response = new JSONObject(data);
542         JSONArray errors = response.getJSONArray("errors");
543         for (int i = 0; i < errors.length(); i++) {
544             res += (errors.getString(i) + "\n");
545         }
546         return res;
547     }
548 
549     /**
550      * Parse log paths from a device object.
551      *
552      * @param device the device object in JSON.
553      * @return a list of {@link LogFileEntry}.
554      * @throws JSONException if any required property is missing.
555      */
parseLogField(JSONObject device)556     private static List<LogFileEntry> parseLogField(JSONObject device) throws JSONException {
557         List<LogFileEntry> logs = new ArrayList<LogFileEntry>();
558         JSONArray logArray = device.optJSONArray("logs");
559         if (logArray == null) {
560             return logs;
561         }
562         for (int i = 0; i < logArray.length(); i++) {
563             JSONObject logObject = logArray.getJSONObject(i);
564             logs.add(new LogFileEntry(logObject));
565         }
566         return logs;
567     }
568 
569     @VisibleForTesting
determineAcloudErrorType(String errorType)570     static InfraErrorIdentifier determineAcloudErrorType(String errorType) {
571         InfraErrorIdentifier identifier;
572         if (errorType == null || errorType.isEmpty()) {
573             return InfraErrorIdentifier.ACLOUD_UNRECOGNIZED_ERROR_TYPE;
574         }
575         try {
576             identifier = InfraErrorIdentifier.valueOf(errorType);
577         } catch (Exception e) {
578             identifier = InfraErrorIdentifier.ACLOUD_UNRECOGNIZED_ERROR_TYPE;
579         }
580         return identifier;
581     }
582 
583     @VisibleForTesting
addCfStartTimeMetrics(JSONObject json)584     static void addCfStartTimeMetrics(JSONObject json) {
585         // These metrics may not be available for all GCE.
586         String fetch_artifact_time = json.optString("fetch_artifact_time");
587         if (!fetch_artifact_time.isEmpty()) {
588             InvocationMetricLogger.addInvocationMetrics(
589                     InvocationMetricKey.CF_FETCH_ARTIFACT_TIME,
590                     Double.valueOf(Double.parseDouble(fetch_artifact_time) * 1000).longValue());
591         }
592         String gce_create_time = json.optString("gce_create_time");
593         if (!gce_create_time.isEmpty()) {
594             InvocationMetricLogger.addInvocationMetrics(
595                     InvocationMetricKey.CF_GCE_CREATE_TIME,
596                     Double.valueOf(Double.parseDouble(gce_create_time) * 1000).longValue());
597         }
598         String launch_cvd_time = json.optString("launch_cvd_time");
599         if (!launch_cvd_time.isEmpty()) {
600             InvocationMetricLogger.addInvocationMetrics(
601                     InvocationMetricKey.CF_LAUNCH_CVD_TIME,
602                     Double.valueOf(Double.parseDouble(launch_cvd_time) * 1000).longValue());
603         }
604         JSONObject fetch_cvd_wrapper_log = json.optJSONObject("fetch_cvd_wrapper_log");
605         if (fetch_cvd_wrapper_log != null) {
606             String cf_cache_wait_time_sec =
607                     fetch_cvd_wrapper_log.optString("cf_cache_wait_time_sec");
608             if (!Strings.isNullOrEmpty(cf_cache_wait_time_sec)) {
609                 InvocationMetricLogger.addInvocationMetrics(
610                         InvocationMetricKey.CF_CACHE_WAIT_TIME,
611                         Integer.parseInt(cf_cache_wait_time_sec));
612             }
613             String cf_artifacts_fetch_source =
614                     fetch_cvd_wrapper_log.optString("cf_artifacts_fetch_source");
615             if (!Strings.isNullOrEmpty(cf_artifacts_fetch_source)) {
616                 InvocationMetricLogger.addInvocationMetrics(
617                         InvocationMetricKey.CF_ARTIFACTS_FETCH_SOURCE, cf_artifacts_fetch_source);
618             }
619         }
620 
621         if (!InvocationMetricLogger.getInvocationMetrics()
622                 .containsKey(InvocationMetricKey.CF_INSTANCE_COUNT.toString())) {
623             InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.CF_INSTANCE_COUNT, 1);
624         }
625     }
626 }
627