• 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("target-image"),
108                         getBuild().getFile("create_snapshot.zip"),
109                         false,
110                         false,
111                         SnapuserdWaitPhase.BLOCK_AFTER_UPDATE,
112                         null);
113         try {
114             updateUtil.updateDevice(null, null);
115 
116             String afterMountBuildId = getDevice().getBuildId();
117             CLog.d(
118                     "Original build id: %s. after mount build id: %s",
119                     originalBuildId, afterMountBuildId);
120         } finally {
121             updateUtil.teardownDevice(getTestInformation());
122         }
123         String afterRevert = getDevice().getBuildId();
124         CLog.d("Original build id: %s. after unmount build id: %s", originalBuildId, afterRevert);
125     }
126 
127     @Ignore
128     @Test
testBlockCompareUpdate()129     public void testBlockCompareUpdate() throws Throwable {
130         String originalBuildId = getDevice().getBuildId();
131         CLog.d("Original build id: %s", originalBuildId);
132 
133         File blockCompare = getCreateSnapshot();
134         File srcImage = getBuild().getFile("src-image");
135         File srcDirectory = ZipUtil2.extractZipToTemp(srcImage, "incremental_src");
136         File targetImage = getBuild().getFile("target-image");
137         File targetDirectory = ZipUtil2.extractZipToTemp(targetImage, "incremental_target");
138 
139         File workDir = FileUtil.createTempDir("block_compare_workdir");
140         try (CloseableTraceScope e2e = new CloseableTraceScope("end_to_end_update")) {
141             List<Callable<Boolean>> callableTasks = new ArrayList<>();
142             for (String partition : srcDirectory.list()) {
143                 File possibleSrc = new File(srcDirectory, partition);
144                 File possibleTarget = new File(targetDirectory, partition);
145                 if (possibleSrc.exists() && possibleTarget.exists()) {
146                     if (PARTITIONS_TO_DIFF.contains(partition)) {
147                         callableTasks.add(
148                                 () -> {
149                                     blockCompare(
150                                             blockCompare, possibleSrc, possibleTarget, workDir);
151                                     TrackResults newRes = new TrackResults();
152                                     if (mDisableVerity) {
153                                         newRes.imageMd5 = FileUtil.calculateMd5(possibleTarget);
154                                     }
155                                     partitionToInfo.put(FileUtil.getBaseName(partition), newRes);
156                                     return true;
157                                 });
158                     }
159                 } else {
160                     CLog.e("Skipping %s no src or target", partition);
161                 }
162             }
163             ParallelDeviceExecutor<Boolean> executor =
164                     new ParallelDeviceExecutor<Boolean>(callableTasks.size());
165             executor.invokeAll(callableTasks, 0, TimeUnit.MINUTES);
166             if (executor.hasErrors()) {
167                 throw executor.getErrors().get(0);
168             }
169             inspectCowPatches(workDir);
170 
171             getDevice().executeShellV2Command("mkdir -p /data/ndb");
172             getDevice().executeShellV2Command("rm -rf /data/ndb/*.patch");
173 
174             // Ensure snapshotctl exists
175             CommandResult whichOutput = getDevice().executeShellV2Command("which snapshotctl");
176             CLog.e("stdout: %s, stderr: %s", whichOutput.getStdout(), whichOutput.getStderr());
177 
178             getDevice().executeShellV2Command("snapshotctl unmap-snapshots");
179             getDevice().executeShellV2Command("snapshotctl delete-snapshots");
180 
181             List<Callable<Boolean>> pushTasks = new ArrayList<>();
182             for (File f : workDir.listFiles()) {
183                 try (CloseableTraceScope ignored = new CloseableTraceScope("push:" + f.getName())) {
184                     pushTasks.add(
185                             () -> {
186                                 boolean success;
187                                 if (f.isDirectory()) {
188                                     success = getDevice().pushDir(f, "/data/ndb/");
189                                 } else {
190                                     success = getDevice().pushFile(f, "/data/ndb/" + f.getName());
191                                 }
192                                 CLog.e(
193                                         "Push successful.: %s. %s->%s",
194                                         success, f, "/data/ndb/" + f.getName());
195                                 assertTrue(success);
196                                 return true;
197                             });
198                 }
199             }
200             ParallelDeviceExecutor<Boolean> pushExec =
201                     new ParallelDeviceExecutor<Boolean>(pushTasks.size());
202             pushExec.invokeAll(pushTasks, 0, TimeUnit.MINUTES);
203             if (pushExec.hasErrors()) {
204                 throw pushExec.getErrors().get(0);
205             }
206 
207             CommandResult mapOutput =
208                     getDevice().executeShellV2Command("snapshotctl map-snapshots /data/ndb/");
209             CLog.e("stdout: %s, stderr: %s", mapOutput.getStdout(), mapOutput.getStderr());
210             if (!CommandStatus.SUCCESS.equals(mapOutput.getStatus())) {
211                 fail("Failed to map the snapshots.");
212             }
213 
214             if (mDisableVerity) {
215                 getDevice().executeAdbCommand("disable-verity");
216             }
217             // flash all static partition in bootloader
218             getDevice().rebootIntoBootloader();
219             Map<String, String> envMap = new HashMap<>();
220             envMap.put("ANDROID_PRODUCT_OUT", targetDirectory.getAbsolutePath());
221             CommandResult fastbootResult =
222                     getDevice()
223                             .executeLongFastbootCommand(
224                                     envMap,
225                                     "flashall",
226                                     "--exclude-dynamic-partitions",
227                                     "--disable-super-optimization");
228             CLog.d("Status: %s", fastbootResult.getStatus());
229             CLog.d("stdout: %s", fastbootResult.getStdout());
230             CLog.d("stderr: %s", fastbootResult.getStderr());
231             getDevice().waitForDeviceAvailable(5 * 60 * 1000L);
232             // Do Validation
233             getDevice().enableAdbRoot();
234             CommandResult psOutput = getDevice().executeShellV2Command("ps -ef | grep snapuserd");
235             CLog.d("stdout: %s, stderr: %s", psOutput.getStdout(), psOutput.getStderr());
236 
237             listMappingAndCompare(partitionToInfo);
238 
239             String afterMountBuildId = getDevice().getBuildId();
240             CLog.d(
241                     "Original build id: %s. after mount build id: %s",
242                     originalBuildId, afterMountBuildId);
243         } finally {
244             try (CloseableTraceScope rev = new CloseableTraceScope("revert_to_previous")) {
245                 revertToPreviousBuild(srcDirectory);
246             } finally {
247                 FileUtil.recursiveDelete(workDir);
248                 FileUtil.recursiveDelete(srcDirectory);
249                 FileUtil.recursiveDelete(targetDirectory);
250             }
251 
252             String afterRevert = getDevice().getBuildId();
253             CLog.d(
254                     "Original build id: %s. after unmount build id: %s",
255                     originalBuildId, afterRevert);
256         }
257     }
258 
blockCompare(File blockCompare, File srcImage, File targetImage, File workDir)259     private void blockCompare(File blockCompare, File srcImage, File targetImage, File workDir) {
260         try (CloseableTraceScope ignored =
261                 new CloseableTraceScope("block_compare:" + srcImage.getName())) {
262             IRunUtil runUtil = new RunUtil();
263             runUtil.setWorkingDir(workDir);
264 
265             CommandResult result =
266                     runUtil.runTimedCmd(
267                             0L,
268                             blockCompare.getAbsolutePath(),
269                             "--source=" + srcImage.getAbsolutePath(),
270                             "--target=" + targetImage.getAbsolutePath());
271             if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
272                 throw new RuntimeException(
273                         String.format("%s\n%s", result.getStdout(), result.getStderr()));
274             }
275             File[] listFiles = workDir.listFiles();
276             CLog.e("%s", Arrays.asList(listFiles));
277         }
278     }
279 
getCreateSnapshot()280     private File getCreateSnapshot() throws IOException {
281         File createSnapshotZip = getBuild().getFile("create_snapshot.zip");
282         if (createSnapshotZip == null) {
283             throw new RuntimeException("Cannot find create_snapshot.zip");
284         }
285         File destDir = ZipUtil2.extractZipToTemp(createSnapshotZip, "create_snapshot");
286         File snapshot = FileUtil.findFile(destDir, "create_snapshot");
287         FileUtil.chmodGroupRWX(snapshot);
288         return snapshot;
289     }
290 
inspectCowPatches(File workDir)291     private void inspectCowPatches(File workDir) throws IOException {
292         File inspectZip = getBuild().getFile("inspect_cow.zip");
293         if (inspectZip == null) {
294             return;
295         }
296         File destDir = ZipUtil2.extractZipToTemp(inspectZip, "inspect_cow_unzip");
297         File inspect = FileUtil.findFile(destDir, "inspect_cow");
298         FileUtil.chmodGroupRWX(inspect);
299         IRunUtil runUtil = new RunUtil();
300         long sizeOfPatches = 0L;
301         try (CloseableTraceScope ignored = new CloseableTraceScope("inspect_cow")) {
302             for (File f : workDir.listFiles()) {
303                 CommandResult result =
304                         runUtil.runTimedCmd(0L, inspect.getAbsolutePath(), f.getAbsolutePath());
305                 CLog.d("Status: %s", result.getStatus());
306                 CLog.d("Stdout: %s", result.getStdout());
307                 CLog.d("Stderr: %s", result.getStderr());
308                 CLog.d("Patch size: %s", f.length());
309                 sizeOfPatches += f.length();
310             }
311             CLog.d("Total size of patches: %s", sizeOfPatches);
312         } finally {
313             FileUtil.recursiveDelete(destDir);
314         }
315     }
316 
listMappingAndCompare(Map<String, TrackResults> partitionToInfo)317     private void listMappingAndCompare(Map<String, TrackResults> partitionToInfo)
318             throws DeviceNotAvailableException {
319         CommandResult lsOutput = getDevice().executeShellV2Command("ls -l /dev/block/mapper/");
320         CLog.d("stdout: %s, stderr: %s", lsOutput.getStdout(), lsOutput.getStderr());
321 
322         if (!mDisableVerity) {
323             return;
324         }
325         String[] lineArray = lsOutput.getStdout().split("\n");
326         for (int i = 0; i < lineArray.length; i++) {
327             String lines = lineArray[i];
328             if (!lines.contains("->")) {
329                 continue;
330             }
331             String[] pieces = lines.split("\\s+");
332             String partition = pieces[7].substring(0, pieces[7].length() - 2);
333             CLog.d("Partition extracted: %s", partition);
334             if (partitionToInfo.containsKey(partition)) {
335                 // Since there is system_a/_b ensure we capture the right one
336                 // for md5 comparison
337                 if ("system".equals(partition)) {
338                     if (!lineArray[i +2].contains("-cow-")) {
339                         continue;
340                     }
341                 }
342                 partitionToInfo.get(partition).mountedBlock = pieces[9];
343             }
344         }
345         CLog.d("Infos: %s", partitionToInfo);
346 
347         StringBuilder errorSummary = new StringBuilder();
348         for (Entry<String, TrackResults> res : partitionToInfo.entrySet()) {
349             if (res.getValue().mountedBlock == null) {
350                 errorSummary.append(String.format("No partition found in mapping for %s", res));
351                 errorSummary.append("\n");
352                 continue;
353             }
354             TrackResults result = res.getValue();
355             CommandResult md5Output =
356                     getDevice().executeShellV2Command("md5sum " + result.mountedBlock);
357             CLog.d("stdout: %s, stderr: %s", md5Output.getStdout(), md5Output.getStderr());
358             if (!CommandStatus.SUCCESS.equals(md5Output.getStatus())) {
359                 fail("Fail to get md5sum from " + result.mountedBlock);
360             }
361             String md5device = md5Output.getStdout().trim().split("\\s+")[0];
362             String message =
363                     String.format(
364                             "partition: %s. device md5: %s, file md5: %s",
365                             res.getKey(), md5device, result.imageMd5);
366             CLog.d(message);
367             if (!md5device.equals(result.imageMd5)) {
368                 errorSummary.append(message);
369                 errorSummary.append("\n");
370             }
371         }
372         if (!errorSummary.isEmpty()) {
373             fail(errorSummary.toString());
374         }
375     }
376 
revertToPreviousBuild(File srcDirectory)377     private void revertToPreviousBuild(File srcDirectory) throws DeviceNotAvailableException {
378         CommandResult revertOutput =
379                 getDevice().executeShellV2Command("snapshotctl revert-snapshots");
380         CLog.d("stdout: %s, stderr: %s", revertOutput.getStdout(), revertOutput.getStderr());
381         getDevice().rebootIntoBootloader();
382         Map<String, String> envMap = new HashMap<>();
383         envMap.put("ANDROID_PRODUCT_OUT", srcDirectory.getAbsolutePath());
384         CommandResult fastbootResult =
385                 getDevice()
386                         .executeLongFastbootCommand(
387                                 envMap,
388                                 "flashall",
389                                 "--exclude-dynamic-partitions",
390                                 "--disable-super-optimization");
391         CLog.d("Status: %s", fastbootResult.getStatus());
392         CLog.d("stdout: %s", fastbootResult.getStdout());
393         CLog.d("stderr: %s", fastbootResult.getStderr());
394         getDevice().waitForDeviceAvailable(5 * 60 * 1000L);
395     }
396 }
397