package io.eguan.srv; /* * #%L * Project eguan * %% * Copyright (C) 2012 - 2017 Oodrive * %% * 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. * #L% */ import io.eguan.utils.RunCmdUtils; import io.eguan.utils.unix.UnixMount; import io.eguan.utils.unix.UnixNbdTarget; import io.eguan.utils.unix.UnixTarget; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.util.HashMap; import java.util.Map; import java.util.Random; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import org.junit.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FsOpsTestHelper extends AbstractIopsTestHelper { public FsOpsTestHelper(final int blockSize, final int numBlocks, final int length) { super(blockSize, numBlocks, length); } protected static final Logger LOGGER = LoggerFactory.getLogger(FsOpsTestHelper.class); final class SafetyBeltTask extends TimerTask { private final UnixTarget unixTarget; private boolean cancelled; Throwable runThrowable; public SafetyBeltTask(final UnixTarget unixTarget) { this.unixTarget = unixTarget; cancelled = false; } @Override public final void run() { try { this.unixTarget.logout(); LOGGER.warn("Logout forced for " + this.unixTarget); } catch (final Throwable t) { this.runThrowable = t; } } } private static String FS_TYPE = "ext4"; private final Map<UnixTarget, SafetyBeltTask> safetyBelts = new HashMap<UnixTarget, SafetyBeltTask>(); private final void createSafetyBelt(final UnixTarget unixTarget) { final SafetyBeltTask safetyBeltTask = new SafetyBeltTask(unixTarget); safetyBelts.put(unixTarget, safetyBeltTask); final Timer timer = new Timer("FailSafe"); timer.schedule(safetyBeltTask, 240 * 1000); // Force logout after 240s } private final void cancelSafetyBelt(final UnixTarget unixTarget) { final SafetyBeltTask safetyBeltTask = safetyBelts.get(unixTarget); if (safetyBeltTask != null) { // Cancel logout safetyBeltTask.cancel(); safetyBeltTask.cancelled = true; } } private final void checkExceptionSafetyBelt(final UnixTarget unixTarget) throws Throwable { final SafetyBeltTask safetyBeltTask = safetyBelts.get(unixTarget); if (safetyBeltTask != null) { // Throws safety belt exception if any if (!safetyBeltTask.cancelled && (safetyBeltTask.runThrowable != null)) { safetyBelts.remove(unixTarget); throw safetyBeltTask.runThrowable; } safetyBelts.remove(unixTarget); } } /** * Login on a target * * @throws IOException * if login failed */ public void loginTarget(final UnixTarget unixTarget) throws IOException { unixTarget.login(); createSafetyBelt(unixTarget); } /** * Logout on a target * * @throws Throwable * if logout failed or if safety belt has thrown an exception */ public void logoutTarget(final UnixTarget unixTarget) throws Throwable { unixTarget.logout(); cancelSafetyBelt(unixTarget); checkExceptionSafetyBelt(unixTarget); } /** * Write a file with random values, size = blockSize * numBlocks * length. * * @return The file which has been created. */ public final File writeTmpFile() throws IOException { final ByteBuffer writeData = ByteBuffer.allocate(blockSize * numBlocks); final Random random = new Random(System.currentTimeMillis()); final File dataDump = File.createTempFile("test", "vold"); try (final FileOutputStream fos = new FileOutputStream(dataDump.getAbsolutePath())) { final FileChannel out = fos.getChannel(); for (int i = 0; i < length; i++) { random.nextBytes(writeData.array()); out.write(writeData); writeData.rewind(); } } return dataDump; } /** * Wait for a file to be created * * @param file * the waited file */ public static void waitForFile(final File file) { Assert.assertTrue(io.eguan.utils.Files.waitForFile(file, 60 * 1000)); } /** * construct file and wait for the -part1 to be created * * @param unixTarget * target * @return the new file created */ public static File createAndWaitTargetPart1(final UnixTarget unixTarget) { // Need to wait for the -part1 to be created (udev) final String targetPart1Str = unixTarget.getDeviceFilePath() + unixTarget.getDevicePart1Suffix(); final File targetPart1 = new File(targetPart1Str); waitForFile(targetPart1); return targetPart1; } /** * construct file and wait for the device to be created * * @param unixTarget * iscsi target * * @return the new file created */ public static File createAndWaitTargetDevice(final UnixTarget unixTarget) { // Need to wait for the device to be created (udev) final File targetDevice = new File(unixTarget.getDeviceFilePath()); waitForFile(targetDevice); return targetDevice; } /** * create a partition on the target with fdisk command * * @param unixTarget * The target. * */ public static void partitionCreate(final UnixTarget unixTarget) throws IOException { // fdisk final String[] fdisk = new String[] { "sudo", "fdisk", unixTarget.getDeviceFilePath() }; RunCmdUtils.runCmd(fdisk, unixTarget, "n \n p \n 1 \n \n \n w\n q\n", new String[] { "LANG", "C", "LANGUAGE", "C" }); } /** * delete and create a new partition on the target with fdisk command * * @param unixTarget * The target. * */ public static void partitionUpdate(final UnixTarget unixTarget) throws IOException { // fdisk final String[] fdisk = new String[] { "sudo", "fdisk", unixTarget.getDeviceFilePath() }; RunCmdUtils.runCmd(fdisk, unixTarget, "d \n n \n p \n 1 \n \n \n w\n q\n", new String[] { "LANG", "C", "LANGUAGE", "C" }); } /** * Format the filesystem on the target * * @param unixTarget * The target. * */ public static void formatFileSystem(final UnixTarget unixTarget, final String targetStr) throws IOException { final String fs = FS_TYPE; // mkfs final String[] mkfs = new String[] { "sudo", "/sbin/mkfs", "-t", fs, targetStr }; RunCmdUtils.runCmd(mkfs, unixTarget, "y\n", new String[] { "LANG", "C", "LANGUAGE", "C" }); } /** * wait for flush of IOs * * @param unixTarget * The target. * */ public static void sync(final UnixTarget unixTarget) throws IOException { // sync: wait for flush of IOs final String[] sync = new String[] { "sudo", "/bin/sync" }; RunCmdUtils.runCmd(sync, unixTarget, true); } /** * Check the file system * * @param unixTarget * The target. * */ public static void fsCheck(final UnixTarget unixTarget) throws IOException { // sync: wait for flush of IOs final String targetPart1Str = unixTarget.getDeviceFilePath() + unixTarget.getDevicePart1Suffix(); final String[] sync = new String[] { "sudo", "e2fsck", "-f", "-p", targetPart1Str }; RunCmdUtils.runCmd(sync, unixTarget, true, true); } /** * Resize the file system * * @param unixTarget * The target. * */ public static void fsResize(final UnixTarget unixTarget, final String targetStr) throws IOException { // sync: wait for flush of IOs final String[] sync = new String[] { "sudo", "resize2fs", targetStr }; RunCmdUtils.runCmd(sync, unixTarget, true); } /** * mount the new remote disk * * @param unixTarget * The target. * @param mountPoint * mountPoint * * @return the new file created from the mount point */ public static UnixMount mountDiscOnTarget(final UnixTarget unixTarget, final String targetStr, final File mountPoint, final String option) throws IOException { final String fs = FS_TYPE; // mount final UnixMount unixMount = new UnixMount(mountPoint, targetStr, option, fs); unixMount.mount(); // Directory lost+found of ext partitions Assert.assertTrue(new File(mountPoint, "lost+found").isDirectory()); return unixMount; } /** * Compare a file with a another file on the target * * @param unixTarget * the target * @param File * the file to compare * @param mountPoint * the mount point directory * @throws IOException * if the two files are different * */ public static void compareFileOnTarget(final UnixTarget unixTarget, final File file, final File mountPoint) throws IOException { final String[] cmp = new String[] { "sudo", "cmp", file.getAbsolutePath(), new File(mountPoint, file.getName()).getAbsolutePath() }; RunCmdUtils.runCmd(cmp, unixTarget, true); } /** * Write a file on the target and compare the two files * * @param unixTarget * the target * @param file * the file which must be copied * @param mountPoint * the mount point on which the file must be copied * * @throws IOException * if the two files are different */ public static void writeFileOnTarget(final UnixTarget unixTarget, final File file, final File mountPoint) throws IOException { final String[] cp = new String[] { "sudo", "cp", file.getAbsolutePath(), mountPoint.getAbsolutePath() }; RunCmdUtils.runCmd(cp, unixTarget, true); compareFileOnTarget(unixTarget, file, mountPoint); } /** * Remove a file on the target. * * @param unixTarget * the target * @param file * the file which must be removed * @param mountPoint * the mount point on which the file must be removed * * @throws IOException * if the two files are different */ public static void removeFileOnTarget(final UnixTarget unixTarget, final File file, final File mountPoint) throws IOException { final String[] rm = new String[] { "sudo", "rm", new File(mountPoint, file.getName()).getAbsolutePath() }; RunCmdUtils.runCmd(rm, unixTarget, true); } /** * Get current kernel version. * * @param unixTarget * * @return the kernel version * * @throws IOException */ public static StringBuilder getKernelVersion(final UnixTarget unixTarget) throws IOException { final String[] rm = new String[] { "uname", "-r" }; return RunCmdUtils.runCmd(rm, unixTarget, true); } /** * Write some data on a dev file. * * @param unixTarget * the target * @param size * the number of bytes to write * @param count * * @return * @throws IOException */ public static StringBuilder writeData(final UnixTarget unixTarget, final int size, final int count) throws IOException { final String[] dd = new String[] { "sudo", "dd", "if=/dev/zero", "of=" + unixTarget.getDeviceFilePath(), "bs=" + size, "count=" + count, "oflag=sync" }; return RunCmdUtils.runCmd(dd, unixTarget, true, true); } /** * Test to read/write a file on a target. * * @param unixTarget * the target * @throws Throwable */ public File testReadWriteFile(final UnixTarget unixTarget) throws Throwable { final File mountPoint = Files.createTempDirectory("mount").toFile(); try { loginTarget(unixTarget); try { final File targetDevice = FsOpsTestHelper.createAndWaitTargetDevice(unixTarget); partitionCreate(unixTarget); final File targetPart1 = createAndWaitTargetPart1(unixTarget); final String targetPart1Str = unixTarget.getDeviceFilePath() + unixTarget.getDevicePart1Suffix(); formatFileSystem(unixTarget, targetPart1Str); sync(unixTarget); final UnixMount unixMount = mountDiscOnTarget(unixTarget, targetPart1Str, mountPoint, null); boolean isMount = true; try { // Create temp file, cp file, read/compare file final File tmpFile = writeTmpFile(); try { writeFileOnTarget(unixTarget, tmpFile, mountPoint); // umount unixMount.umount(); isMount = false; unixTarget.logout(); unixTarget.login(); // Need to wait for the device to be created (udev) waitForFile(targetDevice); waitForFile(targetPart1); // mount & umount unixMount.mount(); isMount = true; Assert.assertTrue(new File(mountPoint, "lost+found").isDirectory()); // Read/compare file again compareFileOnTarget(unixTarget, tmpFile, mountPoint); return tmpFile; } catch (final Throwable t) { tmpFile.delete(); throw t; } } finally { if (isMount) { unixMount.umount(); } } } finally { logoutTarget(unixTarget); } } finally { mountPoint.delete(); } } /** * Test trim command. * * @param unixNbdTarget * the target * @param checkSrvCmd * interface to check commands * @throws Throwable */ public void testNbdTrim(final UnixNbdTarget unixNbdTarget, final CheckSrvCommand checkSrvCmd) throws Throwable { final File mountPoint = Files.createTempDirectory("mount").toFile(); try { loginTarget(unixNbdTarget); try { createAndWaitTargetDevice(unixNbdTarget); partitionCreate(unixNbdTarget); createAndWaitTargetPart1(unixNbdTarget); final String targetPart1Str = unixNbdTarget.getDeviceFilePath() + unixNbdTarget.getDevicePart1Suffix(); formatFileSystem(unixNbdTarget, targetPart1Str); sync(unixNbdTarget); final UnixMount unixMount = FsOpsTestHelper.mountDiscOnTarget(unixNbdTarget, targetPart1Str, mountPoint, "data=journal,discard"); try { if (checkSrvCmd != null) { checkSrvCmd.checkTrim(unixNbdTarget, 0, false); } // Create temp file, cp file, read/compare file final File tmpFile = writeTmpFile(); try { writeFileOnTarget(unixNbdTarget, tmpFile, mountPoint); sync(unixNbdTarget); // A second sync is necessary to flush all the data sync(unixNbdTarget); removeFileOnTarget(unixNbdTarget, tmpFile, mountPoint); sync(unixNbdTarget); if (checkSrvCmd != null) { checkSrvCmd.checkTrim(unixNbdTarget, blockSize * numBlocks * length, true); } } finally { tmpFile.delete(); } } finally { unixMount.umount(); } } finally { logoutTarget(unixNbdTarget); } } finally { mountPoint.delete(); } } /** * R/W multi thread method. * * @param executor * @param unixTarget * * @return the Future to wait the result */ public Future<File> multiThreadRW(final ExecutorService executor, final UnixTarget unixTarget) { final Future<File> future = executor.submit(new Callable<File>() { @Override public File call() throws Exception { try { return testReadWriteFile(unixTarget); } catch (final Throwable t) { throw new Exception(t); } } }); return future; } }