hudson.os.solaris.ZFSInstaller.java Source code

Java tutorial

Introduction

Here is the source code for hudson.os.solaris.ZFSInstaller.java

Source

/*
 * The MIT License
 *
 * Copyright (c) 2004-2009, Sun Microsystems, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package hudson.os.solaris;

import com.sun.akuma.Daemon;
import com.sun.akuma.JavaVMArguments;
import hudson.Launcher.LocalLauncher;
import hudson.Util;
import hudson.Extension;
import hudson.os.SU;
import hudson.model.AdministrativeMonitor;
import hudson.model.Hudson;
import hudson.model.TaskListener;
import hudson.remoting.Callable;
import hudson.util.ForkOutputStream;
import hudson.util.HudsonIsRestarting;
import hudson.util.StreamTaskListener;
import static hudson.util.jna.GNUCLibrary.*;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.jvnet.libpam.impl.CLibrary.passwd;
import org.jvnet.solaris.libzfs.ACLBuilder;
import org.jvnet.solaris.libzfs.LibZFS;
import org.jvnet.solaris.libzfs.ZFSException;
import org.jvnet.solaris.libzfs.ZFSFileSystem;
import org.jvnet.solaris.libzfs.ErrorCode;
import org.jvnet.solaris.mount.MountFlags;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpRedirect;

import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Encourages the user to migrate HUDSON_HOME on a ZFS file system. 
 *
 * @author Kohsuke Kawaguchi
 * @since 1.283
 */
public class ZFSInstaller extends AdministrativeMonitor implements Serializable {
    /**
     * True if $HUDSON_HOME is a ZFS file system by itself.
     */
    private final boolean active = shouldBeActive();

    /**
     * This will be the file system name that we'll create.
     */
    private String prospectiveZfsFileSystemName;

    public boolean isActivated() {
        return active;
    }

    public boolean isRoot() {
        return LIBC.geteuid() == 0;
    }

    public String getProspectiveZfsFileSystemName() {
        return prospectiveZfsFileSystemName;
    }

    private boolean shouldBeActive() {
        if (!System.getProperty("os.name").equals("SunOS") || disabled)
            // on systems that don't have ZFS, we don't need this monitor
            return false;

        try {
            LibZFS zfs = new LibZFS();
            List<ZFSFileSystem> roots = zfs.roots();
            if (roots.isEmpty())
                return false; // no active ZFS pool

            // if we don't run on a ZFS file system, activate
            ZFSFileSystem hudsonZfs = zfs.getFileSystemByMountPoint(Hudson.getInstance().getRootDir());
            if (hudsonZfs != null)
                return false; // already on ZFS

            // decide what file system we'll create
            ZFSFileSystem pool = roots.get(0);
            prospectiveZfsFileSystemName = computeHudsonFileSystemName(zfs, pool);

            return true;
        } catch (Exception e) {
            LOGGER.log(Level.WARNING, "Failed to detect whether Hudson is on ZFS", e);
            return false;
        } catch (LinkageError e) {
            LOGGER.info(
                    "No ZFS available. If you believe this is an error, increase the logging level to get the stack trace");
            LOGGER.log(Level.FINE, "Stack trace of failed ZFS load", e);
            return false;
        }
    }

    /**
     * Called from the management screen.
     */
    public HttpResponse doAct(StaplerRequest req) throws ServletException, IOException {
        requirePOST();
        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);

        if (req.hasParameter("n")) {
            // we'll shut up
            disable(true);
            return HttpRedirect.fromContextPath("/manage");
        }

        return new HttpRedirect("confirm");
    }

    /**
     * Creates a ZFS file system to migrate the data to.
     *
     * <p>
     * This has to be done while we still have an interactive access with the user, since it involves the password.
     *
     * <p>
     * An exception will be thrown if the operation fails. A normal completion means a success.
     *
     * @return
     *      The ZFS dataset name to migrate the data to.
     */
    private String createZfsFileSystem(final TaskListener listener, String rootUsername, String rootPassword)
            throws IOException, InterruptedException, ZFSException {
        // capture the UID that Hudson runs under
        // so that we can allow this user to do everything on this new partition
        final int uid = LIBC.geteuid();
        final int gid = LIBC.getegid();
        passwd pwd = LIBC.getpwuid(uid);
        if (pwd == null)
            throw new IOException("Failed to obtain the current user information for " + uid);
        final String userName = pwd.pw_name;

        final File home = Hudson.getInstance().getRootDir();

        // this is the actual creation of the file system.
        // return true indicating a success
        return SU.execute(listener, rootUsername, rootPassword, new Callable<String, IOException>() {
            public String call() throws IOException {
                PrintStream out = listener.getLogger();

                LibZFS zfs = new LibZFS();
                ZFSFileSystem existing = zfs.getFileSystemByMountPoint(home);
                if (existing != null) {
                    // no need for migration
                    out.println(home + " is already on ZFS. Doing nothing");
                    return existing.getName();
                }

                String name = computeHudsonFileSystemName(zfs, zfs.roots().get(0));
                out.println("Creating " + name);
                ZFSFileSystem hudson = zfs.create(name, ZFSFileSystem.class);

                // mount temporarily to set the owner right
                File dir = Util.createTempDir();
                hudson.setMountPoint(dir);
                hudson.mount();
                if (LIBC.chown(dir.getPath(), uid, gid) != 0)
                    throw new IOException("Failed to chown " + dir);
                hudson.unmount();

                try {
                    hudson.setProperty("hudson:managed-by", "hudson"); // mark this file system as "managed by Hudson"

                    ACLBuilder acl = new ACLBuilder();
                    acl.user(userName).withEverything();
                    hudson.allow(acl);
                } catch (ZFSException e) {
                    // revert the file system creation
                    try {
                        hudson.destory();
                    } catch (Exception _) {
                        // but ignore the error and let the original error thrown
                    }
                    throw e;
                }
                return hudson.getName();
            }
        });
    }

    /**
     * Called from the confirmation screen to actually initiate the migration.
     */
    public void doStart(StaplerRequest req, StaplerResponse rsp, @QueryParameter String username,
            @QueryParameter String password) throws ServletException, IOException {
        requirePOST();
        Hudson hudson = Hudson.getInstance();
        hudson.checkPermission(Hudson.ADMINISTER);

        final String datasetName;
        ByteArrayOutputStream log = new ByteArrayOutputStream();
        StreamTaskListener listener = new StreamTaskListener(log);
        try {
            datasetName = createZfsFileSystem(listener, username, password);
        } catch (Exception e) {
            e.printStackTrace(listener.error(e.getMessage()));

            if (e instanceof ZFSException) {
                ZFSException ze = (ZFSException) e;
                if (ze.getCode() == ErrorCode.EZFS_PERM) {
                    // permission problem. ask the user to give us the root password
                    req.setAttribute("message", log.toString());
                    rsp.forward(this, "askRootPassword", req);
                    return;
                }
            }

            // for other kinds of problems, report and bail out
            req.setAttribute("pre", true);
            sendError(log.toString(), req, rsp);
            return;
        }

        // file system creation successful, so restart

        hudson.servletContext.setAttribute("app", new HudsonIsRestarting());
        // redirect the user to the manage page
        rsp.sendRedirect2(req.getContextPath() + "/manage");

        // asynchronously restart, so that we can give a bit of time to the browser to load "restarting..." screen.
        new Thread("restart thread") {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);

                    // close all descriptors on exec except stdin,out,err
                    int sz = LIBC.getdtablesize();
                    for (int i = 3; i < sz; i++) {
                        int flags = LIBC.fcntl(i, F_GETFD);
                        if (flags < 0)
                            continue;
                        LIBC.fcntl(i, F_SETFD, flags | FD_CLOEXEC);
                    }

                    // re-exec with the system property to indicate where to migrate the data to.
                    // the 2nd phase is implemented in the migrate method.
                    JavaVMArguments args = JavaVMArguments.current();
                    args.setSystemProperty(ZFSInstaller.class.getName() + ".migrate", datasetName);
                    Daemon.selfExec(args);
                } catch (InterruptedException e) {
                    LOGGER.log(Level.SEVERE, "Restart failed", e);
                } catch (IOException e) {
                    LOGGER.log(Level.SEVERE, "Restart failed", e);
                }
            }
        }.start();
    }

    @Extension
    public static AdministrativeMonitor init() {
        String migrationTarget = System.getProperty(ZFSInstaller.class.getName() + ".migrate");
        if (migrationTarget != null) {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            StreamTaskListener listener = new StreamTaskListener(new ForkOutputStream(System.out, out));
            try {
                if (migrate(listener, migrationTarget)) {
                    // completed successfully
                    return new MigrationCompleteNotice();
                }
            } catch (Exception e) {
                // if we let any exception from here, it will prevent Hudson from starting.
                e.printStackTrace(listener.error("Migration failed"));
            }
            // migration failed
            return new MigrationFailedNotice(out);
        }

        // install the monitor if applicable
        ZFSInstaller zi = new ZFSInstaller();
        if (zi.isActivated())
            return zi;

        return null;
    }

    /**
     * Migrates $HUDSON_HOME to a new ZFS file system.
     *
     * TODO: do this in a separate JVM to elevate the privilege.
     *
     * @param listener
     *      Log of migration goes here.
     * @param target
     *      Dataset to move the data to.
     * @return
     *      false if a migration failed.
     */
    private static boolean migrate(TaskListener listener, String target) throws IOException, InterruptedException {
        PrintStream out = listener.getLogger();

        File home = Hudson.getInstance().getRootDir();
        // do the migration
        LibZFS zfs = new LibZFS();
        ZFSFileSystem existing = zfs.getFileSystemByMountPoint(home);
        if (existing != null) {
            out.println(home + " is already on ZFS. Doing nothing");
            return true;
        }

        File tmpDir = Util.createTempDir();

        // mount a new file system to a temporary location
        out.println("Opening " + target);
        ZFSFileSystem hudson = zfs.open(target, ZFSFileSystem.class);
        hudson.setMountPoint(tmpDir);
        hudson.setProperty("hudson:managed-by", "hudson"); // mark this file system as "managed by Hudson"
        hudson.mount();

        // copy all the files
        out.println("Copying all existing data files");
        if (system(home, listener, "/usr/bin/cp", "-pR", ".", tmpDir.getAbsolutePath()) != 0) {
            out.println("Failed to copy " + home + " to " + tmpDir);
            return false;
        }

        // unmount
        out.println("Unmounting " + target);
        hudson.unmount(MountFlags.MS_FORCE);

        // move the original directory to the side
        File backup = new File(home.getPath() + ".backup");
        out.println("Moving " + home + " to " + backup);
        if (backup.exists())
            Util.deleteRecursive(backup);
        if (!home.renameTo(backup)) {
            out.println("Failed to move your current data " + home + " out of the way");
        }

        // update the mount point
        out.println("Creating a new mount point at " + home);
        if (!home.mkdir())
            throw new IOException("Failed to create mount point " + home);

        out.println("Mounting " + target);
        hudson.setMountPoint(home);
        hudson.mount();

        out.println("Sharing " + target);
        try {
            hudson.setProperty("sharesmb", "on");
            hudson.setProperty("sharenfs", "on");
            hudson.share();
        } catch (ZFSException e) {
            listener.error("Failed to share the file systems: " + e.getCode());
        }

        // delete back up
        out.println("Deleting " + backup);
        if (system(new File("/"), listener, "/usr/bin/rm", "-rf", backup.getAbsolutePath()) != 0) {
            out.println("Failed to delete " + backup.getAbsolutePath());
            return false;
        }

        out.println("Migration completed");
        return true;
    }

    private static int system(File pwd, TaskListener listener, String... args)
            throws IOException, InterruptedException {
        return new LocalLauncher(listener).launch().cmds(args).stdout(System.out).pwd(pwd).join();
    }

    private static String computeHudsonFileSystemName(LibZFS zfs, ZFSFileSystem top) {
        if (!zfs.exists(top.getName() + "/hudson"))
            return top.getName() + "/hudson";
        for (int i = 2;; i++) {
            String name = top.getName() + "/hudson" + i;
            if (!zfs.exists(name))
                return name;
        }
    }

    /**
     * Used to indicate that the migration was completed successfully.
     */
    public static final class MigrationCompleteNotice extends AdministrativeMonitor {
        public boolean isActivated() {
            return true;
        }
    }

    /**
     * Used to indicate a failure in the migration.
     */
    public static final class MigrationFailedNotice extends AdministrativeMonitor {
        ByteArrayOutputStream record;

        MigrationFailedNotice(ByteArrayOutputStream record) {
            this.record = record;
        }

        public boolean isActivated() {
            return true;
        }

        public String getLog() {
            return record.toString();
        }
    }

    private static final Logger LOGGER = Logger.getLogger(ZFSInstaller.class.getName());

    /**
     * Escape hatch in case JNI calls fatally crash, like in HUDSON-3733.
     */
    public static boolean disabled = Boolean.getBoolean(ZFSInstaller.class.getName() + ".disabled");
}