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