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.ddmlib.IDevice;
19 import com.android.tradefed.build.BuildRetrievalError;
20 import com.android.tradefed.build.IBuildInfo;
21 import com.android.tradefed.config.Configuration;
22 import com.android.tradefed.config.ConfigurationException;
23 import com.android.tradefed.config.DynamicRemoteFileResolver;
24 import com.android.tradefed.config.IConfiguration;
25 import com.android.tradefed.config.OptionCopier;
26 import com.android.tradefed.device.DeviceNotAvailableException;
27 import com.android.tradefed.device.IDeviceMonitor;
28 import com.android.tradefed.device.IDeviceStateMonitor;
29 import com.android.tradefed.device.StubDevice;
30 import com.android.tradefed.device.TestDevice;
31 import com.android.tradefed.device.TestDeviceOptions;
32 import com.android.tradefed.device.cloud.GceAvdInfo.GceStatus;
33 import com.android.tradefed.log.ITestLogger;
34 import com.android.tradefed.log.LogUtil.CLog;
35 import com.android.tradefed.result.FileInputStreamSource;
36 import com.android.tradefed.result.ITestLoggerReceiver;
37 import com.android.tradefed.result.InputStreamSource;
38 import com.android.tradefed.result.LogDataType;
39 import com.android.tradefed.result.error.DeviceErrorIdentifier;
40 import com.android.tradefed.targetprep.TargetSetupError;
41 import com.android.tradefed.util.FileUtil;
42 import com.android.tradefed.util.StreamUtil;
43 
44 import com.google.common.annotations.VisibleForTesting;
45 
46 import java.io.File;
47 import java.io.IOException;
48 
49 /**
50  * A device running inside a virtual machine that we manage remotely via a Tradefed instance inside
51  * the VM.
52  */
53 public class ManagedRemoteDevice extends TestDevice implements ITestLoggerReceiver {
54 
55     private GceManager mGceHandler = null;
56     private GceAvdInfo mGceAvd;
57     private ITestLogger mTestLogger;
58 
59     private TestDeviceOptions mCopiedOptions;
60     private IConfiguration mValidationConfig;
61 
62     /**
63      * Creates a {@link ManagedRemoteDevice}.
64      *
65      * @param device the associated {@link IDevice}
66      * @param stateMonitor the {@link IDeviceStateMonitor} mechanism to use
67      * @param allocationMonitor the {@link IDeviceMonitor} to inform of allocation state changes.
68      */
ManagedRemoteDevice( IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor)69     public ManagedRemoteDevice(
70             IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor) {
71         super(device, stateMonitor, allocationMonitor);
72     }
73 
74     @Override
preInvocationSetup(IBuildInfo info)75     public void preInvocationSetup(IBuildInfo info)
76             throws TargetSetupError, DeviceNotAvailableException {
77         super.preInvocationSetup(info);
78         mGceAvd = null;
79         // First get the options
80         TestDeviceOptions options = getOptions();
81         // We create a brand new GceManager each time to ensure clean state.
82         mGceHandler = new GceManager(getDeviceDescriptor(), options, info);
83         getGceHandler().logStableHostImageInfos(info);
84         setFastbootEnabled(false);
85 
86         // Launch GCE helper script.
87         long startTime = getCurrentTime();
88         launchGce();
89         long remainingTime = options.getGceCmdTimeout() - (getCurrentTime() - startTime);
90         if (remainingTime < 0) {
91             throw new DeviceNotAvailableException(
92                     String.format("Failed to launch GCE after %sms", options.getGceCmdTimeout()),
93                     getSerialNumber(),
94                     DeviceErrorIdentifier.FAILED_TO_LAUNCH_GCE);
95         }
96     }
97 
98     /** {@inheritDoc} */
99     @Override
postInvocationTearDown(Throwable exception)100     public void postInvocationTearDown(Throwable exception) {
101         try {
102             CLog.i("Shutting down GCE device %s", getSerialNumber());
103             // Log the last part of the logcat from the tear down.
104             if (!(getIDevice() instanceof StubDevice)) {
105                 try (InputStreamSource logcatSource = getLogcat()) {
106                     clearLogcat();
107                     String name = "device_logcat_teardown_gce";
108                     mTestLogger.testLog(name, LogDataType.LOGCAT, logcatSource);
109                 }
110             }
111 
112             if (mGceAvd != null) {
113                 if (mGceAvd.hostAndPort() != null) {
114                     // attempt to get a bugreport if Gce Avd is a failure
115                     if (!GceStatus.SUCCESS.equals(mGceAvd.getStatus())) {
116                         // Get a bugreport via ssh
117                         getSshBugreport();
118                     }
119                     // Log the serial output of the instance.
120                     getGceHandler().logSerialOutput(mGceAvd, mTestLogger);
121 
122                     // Fetch remote files
123                     CommonLogRemoteFileUtil.fetchCommonFiles(
124                             mTestLogger, mGceAvd, getOptions(), getRunUtil());
125                 }
126                 // Cleanup GCE first to make sure ssh tunnel has nowhere to go.
127                 if (!getOptions().shouldSkipTearDown()) {
128                     getGceHandler().shutdownGce();
129                 }
130             }
131 
132             setFastbootEnabled(false);
133 
134             if (getGceHandler() != null) {
135                 getGceHandler().cleanUp();
136             }
137         } finally {
138             // Reset the internal variable
139             mCopiedOptions = null;
140             if (mValidationConfig != null) {
141                 mValidationConfig.cleanConfigurationData();
142                 mValidationConfig = null;
143             }
144             // Ensure parent postInvocationTearDown is always called.
145             super.postInvocationTearDown(exception);
146         }
147     }
148 
149     @Override
setTestLogger(ITestLogger testLogger)150     public void setTestLogger(ITestLogger testLogger) {
151         mTestLogger = testLogger;
152     }
153 
154     /** Returns the {@link GceAvdInfo} describing the remote instance. */
getRemoteAvdInfo()155     public GceAvdInfo getRemoteAvdInfo() {
156         return mGceAvd;
157     }
158 
159     /** Launch the actual gce device based on the build info. */
launchGce()160     protected void launchGce() throws TargetSetupError {
161         TargetSetupError exception = null;
162         for (int attempt = 0; attempt < getOptions().getGceMaxAttempt(); attempt++) {
163             try {
164                 mGceAvd = getGceHandler().startGce();
165                 if (mGceAvd != null) break;
166             } catch (TargetSetupError tse) {
167                 CLog.w(
168                         "Failed to start Gce with attempt: %s out of %s. With Exception: %s",
169                         attempt + 1, getOptions().getGceMaxAttempt(), tse);
170                 exception = tse;
171             }
172         }
173         if (mGceAvd == null) {
174             throw exception;
175         } else {
176             CLog.i("GCE AVD has been started: %s", mGceAvd);
177             if (GceAvdInfo.GceStatus.BOOT_FAIL.equals(mGceAvd.getStatus())) {
178                 throw new TargetSetupError(mGceAvd.getErrors(), getDeviceDescriptor());
179             }
180         }
181     }
182 
183     /** Capture a remote bugreport by ssh-ing into the device directly. */
getSshBugreport()184     private void getSshBugreport() {
185         File bugreportFile = null;
186         try {
187             bugreportFile =
188                     GceManager.getNestedDeviceSshBugreportz(mGceAvd, getOptions(), getRunUtil());
189             if (bugreportFile != null) {
190                 InputStreamSource bugreport = new FileInputStreamSource(bugreportFile);
191                 mTestLogger.testLog("bugreportz-ssh", LogDataType.BUGREPORTZ, bugreport);
192                 StreamUtil.cancel(bugreport);
193             }
194         } catch (IOException e) {
195             CLog.e(e);
196         } finally {
197             FileUtil.deleteFile(bugreportFile);
198         }
199     }
200 
201     /** Returns the current system time. Exposed for testing. */
202     @VisibleForTesting
getCurrentTime()203     protected long getCurrentTime() {
204         return System.currentTimeMillis();
205     }
206 
207     /** Returns the instance of the {@link GceManager}. */
208     @VisibleForTesting
getGceHandler()209     GceManager getGceHandler() {
210         return mGceHandler;
211     }
212 
213     /**
214      * Override the base getter to be able to resolve dynamic options before attempting to do the
215      * remote setup.
216      */
217     @Override
getOptions()218     public TestDeviceOptions getOptions() {
219         if (mCopiedOptions == null) {
220             mCopiedOptions = new TestDeviceOptions();
221             TestDeviceOptions options = super.getOptions();
222             OptionCopier.copyOptionsNoThrow(options, mCopiedOptions);
223             mValidationConfig = new Configuration("validation", "validation");
224             mValidationConfig.setDeviceOptions(mCopiedOptions);
225             try {
226                 mValidationConfig.resolveDynamicOptions(new DynamicRemoteFileResolver());
227             } catch (BuildRetrievalError | ConfigurationException e) {
228                 throw new RuntimeException(e);
229             }
230         }
231         return mCopiedOptions;
232     }
233 }
234