• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.targetprep.sync;
17 
18 import static org.junit.Assert.assertTrue;
19 import static org.junit.Assert.fail;
20 
21 import com.android.tradefed.config.Option;
22 import com.android.tradefed.device.DeviceNotAvailableException;
23 import com.android.tradefed.device.SnapuserdWaitPhase;
24 import com.android.tradefed.invoker.tracing.CloseableTraceScope;
25 import com.android.tradefed.log.LogUtil.CLog;
26 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
27 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
28 import com.android.tradefed.util.CommandResult;
29 import com.android.tradefed.util.CommandStatus;
30 import com.android.tradefed.util.FileUtil;
31 import com.android.tradefed.util.IRunUtil;
32 import com.android.tradefed.util.RunUtil;
33 import com.android.tradefed.util.ZipUtil2;
34 import com.android.tradefed.util.executor.ParallelDeviceExecutor;
35 import com.android.tradefed.util.image.IncrementalImageUtil;
36 
37 import com.google.common.collect.ImmutableSet;
38 
39 import org.junit.After;
40 import org.junit.Ignore;
41 import org.junit.Test;
42 import org.junit.runner.RunWith;
43 
44 import java.io.File;
45 import java.io.IOException;
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.HashMap;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Map.Entry;
52 import java.util.Set;
53 import java.util.concurrent.Callable;
54 import java.util.concurrent.ConcurrentHashMap;
55 import java.util.concurrent.TimeUnit;
56 
57 /** Basic test to start iterating on device incremental image. */
58 @RunWith(DeviceJUnit4ClassRunner.class)
59 public class IncrementalImageFuncTest extends BaseHostJUnit4Test {
60 
61     @Option(name = "disable-verity")
62     private boolean mDisableVerity = true;
63 
64     @Option(name = "apply-snapshot")
65     private boolean mApplySnapshot = false;
66 
67     public static final Set<String> PARTITIONS_TO_DIFF =
68             ImmutableSet.of(
69                     "product.img",
70                     "system.img",
71                     "system_dlkm.img",
72                     "system_ext.img",
73                     "vendor.img",
74                     "vendor_dlkm.img");
75 
76     public static class TrackResults {
77         public String imageMd5;
78         public String mountedBlock;
79 
80         @Override
toString()81         public String toString() {
82             return "TrackResults [imageMd5=" + imageMd5 + ", mountedBlock=" + mountedBlock + "]";
83         }
84     }
85 
86     private Map<String, TrackResults> partitionToInfo = new ConcurrentHashMap<>();
87 
88     @After
teardown()89     public void teardown() throws DeviceNotAvailableException {
90         if (mDisableVerity) {
91             getDevice().enableAdbRoot();
92             // Reenable verity in case it was disabled.
93             getDevice().executeAdbCommand("enable-verity");
94             getDevice().reboot();
95         }
96     }
97 
98     @Test
testBlockUtility()99     public void testBlockUtility() throws Throwable {
100         String originalBuildId = getDevice().getBuildId();
101         CLog.d("Original build id: %s", originalBuildId);
102 
103         IncrementalImageUtil.isSnapshotInUse(getDevice());
104         IncrementalImageUtil updateUtil =
105                 new IncrementalImageUtil(
106                         getDevice(),
107                         getBuild().getFile("src-image"),
108                         null,
109                         null,
110                         getBuild().getFile("target-image"),
111                         getBuild().getFile("create_snapshot.zip"),
112                         mApplySnapshot,
113                         SnapuserdWaitPhase.BLOCK_AFTER_UPDATE);
114         try {
115             updateUtil.updateDevice(null, null);
116 
117             String afterMountBuildId = getDevice().getBuildId();
118             CLog.d(
119                     "Original build id: %s. after mount build id: %s",
120                     originalBuildId, afterMountBuildId);
121         } finally {
122             updateUtil.teardownDevice(getTestInformation());
123         }
124         String afterRevert = getDevice().getBuildId();
125         CLog.d("Original build id: %s. after unmount build id: %s", originalBuildId, afterRevert);
126     }
127 
128     @Ignore
129     @Test
testBlockCompareUpdate()130     public void testBlockCompareUpdate() throws Throwable {
131         String originalBuildId = getDevice().getBuildId();
132         CLog.d("Original build id: %s", originalBuildId);
133 
134         File blockCompare = getCreateSnapshot();
135         File srcImage = getBuild().getFile("src-image");
136         File srcDirectory = ZipUtil2.extractZipToTemp(srcImage, "incremental_src");
137         File targetImage = getBuild().getFile("target-image");
138         File targetDirectory = ZipUtil2.extractZipToTemp(targetImage, "incremental_target");
139 
140         File workDir = FileUtil.createTempDir("block_compare_workdir");
141         try (CloseableTraceScope e2e = new CloseableTraceScope("end_to_end_update")) {
142             List<Callable<Boolean>> callableTasks = new ArrayList<>();
143             for (String partition : srcDirectory.list()) {
144                 File possibleSrc = new File(srcDirectory, partition);
145                 File possibleTarget = new File(targetDirectory, partition);
146                 if (possibleSrc.exists() && possibleTarget.exists()) {
147                     if (PARTITIONS_TO_DIFF.contains(partition)) {
148                         callableTasks.add(
149                                 () -> {
150                                     blockCompare(
151                                             blockCompare, possibleSrc, possibleTarget, workDir);
152                                     TrackResults newRes = new TrackResults();
153                                     if (mDisableVerity) {
154                                         newRes.imageMd5 = FileUtil.calculateMd5(possibleTarget);
155                                     }
156                                     partitionToInfo.put(FileUtil.getBaseName(partition), newRes);
157                                     return true;
158                                 });
159                     }
160                 } else {
161                     CLog.e("Skipping %s no src or target", partition);
162                 }
163             }
164             ParallelDeviceExecutor<Boolean> executor =
165                     new ParallelDeviceExecutor<Boolean>(callableTasks.size());
166             executor.invokeAll(callableTasks, 0, TimeUnit.MINUTES);
167             if (executor.hasErrors()) {
168                 throw executor.getErrors().get(0);
169             }
170             inspectCowPatches(workDir);
171 
172             getDevice().executeShellV2Command("mkdir -p /data/ndb");
173             getDevice().executeShellV2Command("rm -rf /data/ndb/*.patch");
174 
175             // Ensure snapshotctl exists
176             CommandResult whichOutput = getDevice().executeShellV2Command("which snapshotctl");
177             CLog.e("stdout: %s, stderr: %s", whichOutput.getStdout(), whichOutput.getStderr());
178 
179             getDevice().executeShellV2Command("snapshotctl unmap-snapshots");
180             getDevice().executeShellV2Command("snapshotctl delete-snapshots");
181 
182             List<Callable<Boolean>> pushTasks = new ArrayList<>();
183             for (File f : workDir.listFiles()) {
184                 try (CloseableTraceScope ignored = new CloseableTraceScope("push:" + f.getName())) {
185                     pushTasks.add(
186                             () -> {
187                                 boolean success;
188                                 if (f.isDirectory()) {
189                                     success = getDevice().pushDir(f, "/data/ndb/");
190                                 } else {
191                                     success = getDevice().pushFile(f, "/data/ndb/" + f.getName());
192                                 }
193                                 CLog.e(
194                                         "Push successful.: %s. %s->%s",
195                                         success, f, "/data/ndb/" + f.getName());
196                                 assertTrue(success);
197                                 return true;
198                             });
199                 }
200             }
201             ParallelDeviceExecutor<Boolean> pushExec =
202                     new ParallelDeviceExecutor<Boolean>(pushTasks.size());
203             pushExec.invokeAll(pushTasks, 0, TimeUnit.MINUTES);
204             if (pushExec.hasErrors()) {
205                 throw pushExec.getErrors().get(0);
206             }
207 
208             CommandResult mapOutput =
209                     getDevice().executeShellV2Command("snapshotctl map-snapshots /data/ndb/");
210             CLog.e("stdout: %s, stderr: %s", mapOutput.getStdout(), mapOutput.getStderr());
211             if (!CommandStatus.SUCCESS.equals(mapOutput.getStatus())) {
212                 fail("Failed to map the snapshots.");
213             }
214 
215             if (mDisableVerity) {
216                 getDevice().executeAdbCommand("disable-verity");
217             }
218             // flash all static partition in bootloader
219             getDevice().rebootIntoBootloader();
220             Map<String, String> envMap = new HashMap<>();
221             envMap.put("ANDROID_PRODUCT_OUT", targetDirectory.getAbsolutePath());
222             CommandResult fastbootResult =
223                     getDevice()
224                             .executeLongFastbootCommand(
225                                     envMap,
226                                     "flashall",
227                                     "--exclude-dynamic-partitions",
228                                     "--disable-super-optimization");
229             CLog.d("Status: %s", fastbootResult.getStatus());
230             CLog.d("stdout: %s", fastbootResult.getStdout());
231             CLog.d("stderr: %s", fastbootResult.getStderr());
232             getDevice().waitForDeviceAvailable(5 * 60 * 1000L);
233             // Do Validation
234             getDevice().enableAdbRoot();
235             CommandResult psOutput = getDevice().executeShellV2Command("ps -ef | grep snapuserd");
236             CLog.d("stdout: %s, stderr: %s", psOutput.getStdout(), psOutput.getStderr());
237 
238             listMappingAndCompare(partitionToInfo);
239 
240             String afterMountBuildId = getDevice().getBuildId();
241             CLog.d(
242                     "Original build id: %s. after mount build id: %s",
243                     originalBuildId, afterMountBuildId);
244         } finally {
245             try (CloseableTraceScope rev = new CloseableTraceScope("revert_to_previous")) {
246                 revertToPreviousBuild(srcDirectory);
247             } finally {
248                 FileUtil.recursiveDelete(workDir);
249                 FileUtil.recursiveDelete(srcDirectory);
250                 FileUtil.recursiveDelete(targetDirectory);
251             }
252 
253             String afterRevert = getDevice().getBuildId();
254             CLog.d(
255                     "Original build id: %s. after unmount build id: %s",
256                     originalBuildId, afterRevert);
257         }
258     }
259 
blockCompare(File blockCompare, File srcImage, File targetImage, File workDir)260     private void blockCompare(File blockCompare, File srcImage, File targetImage, File workDir) {
261         try (CloseableTraceScope ignored =
262                 new CloseableTraceScope("block_compare:" + srcImage.getName())) {
263             IRunUtil runUtil = new RunUtil();
264             runUtil.setWorkingDir(workDir);
265 
266             CommandResult result =
267                     runUtil.runTimedCmd(
268                             0L,
269                             blockCompare.getAbsolutePath(),
270                             "--source=" + srcImage.getAbsolutePath(),
271                             "--target=" + targetImage.getAbsolutePath());
272             if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
273                 throw new RuntimeException(
274                         String.format("%s\n%s", result.getStdout(), result.getStderr()));
275             }
276             File[] listFiles = workDir.listFiles();
277             CLog.e("%s", Arrays.asList(listFiles));
278         }
279     }
280 
getCreateSnapshot()281     private File getCreateSnapshot() throws IOException {
282         File createSnapshotZip = getBuild().getFile("create_snapshot.zip");
283         if (createSnapshotZip == null) {
284             throw new RuntimeException("Cannot find create_snapshot.zip");
285         }
286         File destDir = ZipUtil2.extractZipToTemp(createSnapshotZip, "create_snapshot");
287         File snapshot = FileUtil.findFile(destDir, "create_snapshot");
288         FileUtil.chmodGroupRWX(snapshot);
289         return snapshot;
290     }
291 
inspectCowPatches(File workDir)292     private void inspectCowPatches(File workDir) throws IOException {
293         File inspectZip = getBuild().getFile("inspect_cow.zip");
294         if (inspectZip == null) {
295             return;
296         }
297         File destDir = ZipUtil2.extractZipToTemp(inspectZip, "inspect_cow_unzip");
298         File inspect = FileUtil.findFile(destDir, "inspect_cow");
299         FileUtil.chmodGroupRWX(inspect);
300         IRunUtil runUtil = new RunUtil();
301         long sizeOfPatches = 0L;
302         try (CloseableTraceScope ignored = new CloseableTraceScope("inspect_cow")) {
303             for (File f : workDir.listFiles()) {
304                 CommandResult result =
305                         runUtil.runTimedCmd(0L, inspect.getAbsolutePath(), f.getAbsolutePath());
306                 CLog.d("Status: %s", result.getStatus());
307                 CLog.d("Stdout: %s", result.getStdout());
308                 CLog.d("Stderr: %s", result.getStderr());
309                 CLog.d("Patch size: %s", f.length());
310                 sizeOfPatches += f.length();
311             }
312             CLog.d("Total size of patches: %s", sizeOfPatches);
313         } finally {
314             FileUtil.recursiveDelete(destDir);
315         }
316     }
317 
listMappingAndCompare(Map<String, TrackResults> partitionToInfo)318     private void listMappingAndCompare(Map<String, TrackResults> partitionToInfo)
319             throws DeviceNotAvailableException {
320         CommandResult lsOutput = getDevice().executeShellV2Command("ls -l /dev/block/mapper/");
321         CLog.d("stdout: %s, stderr: %s", lsOutput.getStdout(), lsOutput.getStderr());
322 
323         if (!mDisableVerity) {
324             return;
325         }
326         String[] lineArray = lsOutput.getStdout().split("\n");
327         for (int i = 0; i < lineArray.length; i++) {
328             String lines = lineArray[i];
329             if (!lines.contains("->")) {
330                 continue;
331             }
332             String[] pieces = lines.split("\\s+");
333             String partition = pieces[7].substring(0, pieces[7].length() - 2);
334             CLog.d("Partition extracted: %s", partition);
335             if (partitionToInfo.containsKey(partition)) {
336                 // Since there is system_a/_b ensure we capture the right one
337                 // for md5 comparison
338                 if ("system".equals(partition)) {
339                     if (!lineArray[i +2].contains("-cow-")) {
340                         continue;
341                     }
342                 }
343                 partitionToInfo.get(partition).mountedBlock = pieces[9];
344             }
345         }
346         CLog.d("Infos: %s", partitionToInfo);
347 
348         StringBuilder errorSummary = new StringBuilder();
349         for (Entry<String, TrackResults> res : partitionToInfo.entrySet()) {
350             if (res.getValue().mountedBlock == null) {
351                 errorSummary.append(String.format("No partition found in mapping for %s", res));
352                 errorSummary.append("\n");
353                 continue;
354             }
355             TrackResults result = res.getValue();
356             CommandResult md5Output =
357                     getDevice().executeShellV2Command("md5sum " + result.mountedBlock);
358             CLog.d("stdout: %s, stderr: %s", md5Output.getStdout(), md5Output.getStderr());
359             if (!CommandStatus.SUCCESS.equals(md5Output.getStatus())) {
360                 fail("Fail to get md5sum from " + result.mountedBlock);
361             }
362             String md5device = md5Output.getStdout().trim().split("\\s+")[0];
363             String message =
364                     String.format(
365                             "partition: %s. device md5: %s, file md5: %s",
366                             res.getKey(), md5device, result.imageMd5);
367             CLog.d(message);
368             if (!md5device.equals(result.imageMd5)) {
369                 errorSummary.append(message);
370                 errorSummary.append("\n");
371             }
372         }
373         if (!errorSummary.isEmpty()) {
374             fail(errorSummary.toString());
375         }
376     }
377 
revertToPreviousBuild(File srcDirectory)378     private void revertToPreviousBuild(File srcDirectory) throws DeviceNotAvailableException {
379         CommandResult revertOutput =
380                 getDevice().executeShellV2Command("snapshotctl revert-snapshots");
381         CLog.d("stdout: %s, stderr: %s", revertOutput.getStdout(), revertOutput.getStderr());
382         getDevice().rebootIntoBootloader();
383         Map<String, String> envMap = new HashMap<>();
384         envMap.put("ANDROID_PRODUCT_OUT", srcDirectory.getAbsolutePath());
385         CommandResult fastbootResult =
386                 getDevice()
387                         .executeLongFastbootCommand(
388                                 envMap,
389                                 "flashall",
390                                 "--exclude-dynamic-partitions",
391                                 "--disable-super-optimization");
392         CLog.d("Status: %s", fastbootResult.getStatus());
393         CLog.d("stdout: %s", fastbootResult.getStdout());
394         CLog.d("stderr: %s", fastbootResult.getStderr());
395         getDevice().waitForDeviceAvailable(5 * 60 * 1000L);
396     }
397 }
398