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