blob: a21ce577b74f35d0b9d4a46092bc3bd425f8ee4b [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.sts.common;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertNotNull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.google.common.hash.Hashing;
/** TestWatcher that enables writing to read-only partitions and reboots device when done. */
public class OverlayFsUtils extends TestWatcher {
private static final String OVERLAYFS_PREFIX = "overlay_sts_";
private final BaseHostJUnit4Test test;
// output of `stat`, e.g. "root shell 755 u:object_r:vendor_file:s0"
static final Pattern PERM_PATTERN =
Pattern.compile(
"^(?<user>[a-zA-Z0-9_-]+) (?<group>[a-zA-Z0-9_-]+) (?<perm>[0-7]+)"
+ " (?<secontext>.*)$");
public OverlayFsUtils(BaseHostJUnit4Test test) {
assertNotNull("Need to pass in a valid testcase object.", test);
this.test = test;
}
/**
* Mounts an OverlayFS dir over the top most common dir in the list.
*
* <p>The directory should be writable after this returns successfully. To cleanup, reboot the
* device as unfortunately unmounting overlayfs is complicated.
*
* @param dir The directory to make writable. Directories with single quotes are not supported.
*/
public void makeWritable(final String dir)
throws DeviceNotAvailableException, IOException, IllegalStateException {
ITestDevice device = test.getDevice();
assertNotNull("device not set.", device);
assertTrue("dir needs to be an absolute path.", dir.startsWith("/"));
// Check and make sure we have not already mounted over this dir. We do that by hashing
// the lower dir path and put that as part of the device ID for `mount`.
String dirHash = Hashing.md5().hashString(dir, StandardCharsets.UTF_8).toString();
String id = OVERLAYFS_PREFIX + dirHash;
CommandResult res = device.executeShellV2Command("mount | grep -q " + id);
if (res.getStatus() == CommandStatus.SUCCESS) {
// a mount with the same ID already exists
throw new IllegalStateException(dir + " has already been made writable.");
}
assertTrue("Can't acquire root for " + device.getSerialNumber(), device.enableAdbRoot());
// Match permissions of upper dir to lower dir
String statOut =
CommandUtil.runAndCheck(device, "stat -c '%U %G %a %C' '" + dir + "'").getStdout();
Matcher m = PERM_PATTERN.matcher(statOut);
assertTrue("Bad stats output: " + statOut, m.find());
String user = m.group("user");
String group = m.group("group");
String unixPerm = m.group("perm");
String seContext = m.group("secontext");
Path tempdir = Paths.get("/mnt", "stsoverlayfs", id);
String upperdir = tempdir.resolve("upper").toString();
String workdir = tempdir.resolve("workdir").toString();
CommandUtil.runAndCheck(device, String.format("mkdir -p '%s' '%s'", upperdir, workdir));
CommandUtil.runAndCheck(device, String.format("chown %s:%s '%s'", user, group, upperdir));
CommandUtil.runAndCheck(device, String.format("chcon '%s' '%s'", seContext, upperdir));
CommandUtil.runAndCheck(device, String.format("chmod %s '%s'", unixPerm, upperdir));
String mountCmd =
String.format(
"mount -t overlay '%s' -o lowerdir='%s',upperdir='%s',workdir='%s' '%s'",
id, dir, upperdir, workdir, dir);
CommandUtil.runAndCheck(device, mountCmd);
}
public boolean anyOverlayFsMounted() throws DeviceNotAvailableException {
ITestDevice device = test.getDevice();
assertNotNull("Device not set", device);
CommandResult res = device.executeShellV2Command("mount | grep -q " + OVERLAYFS_PREFIX);
return res.getStatus() == CommandStatus.SUCCESS;
}
@Override
public void finished(Description d) {
ITestDevice device = test.getDevice();
assertNotNull("Device not set", device);
try {
if (anyOverlayFsMounted()) {
device.rebootUntilOnline();
device.waitForDeviceAvailable();
}
} catch (DeviceNotAvailableException e) {
throw new AssertionError("Device unavailable when cleaning up", e);
}
}
}