ZFSInstaller.java 15.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
/*
 * 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.
 */
24
package hudson.os.solaris;
25 26 27 28 29

import com.sun.akuma.Daemon;
import com.sun.akuma.JavaVMArguments;
import hudson.Launcher.LocalLauncher;
import hudson.Util;
30
import hudson.Extension;
31
import hudson.os.SU;
32 33 34
import hudson.model.AdministrativeMonitor;
import hudson.model.Hudson;
import hudson.model.TaskListener;
35 36
import hudson.remoting.Callable;
import hudson.util.ForkOutputStream;
37 38 39
import hudson.util.HudsonIsRestarting;
import hudson.util.StreamTaskListener;
import static hudson.util.jna.GNUCLibrary.*;
40 41 42
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.jvnet.libpam.impl.CLibrary.passwd;
import org.jvnet.solaris.libzfs.ACLBuilder;
43
import org.jvnet.solaris.libzfs.LibZFS;
44
import org.jvnet.solaris.libzfs.ZFSException;
45
import org.jvnet.solaris.libzfs.ZFSFileSystem;
46
import org.jvnet.solaris.libzfs.ErrorCode;
47 48 49
import org.jvnet.solaris.mount.MountFlags;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
50
import org.kohsuke.stapler.QueryParameter;
51 52 53 54 55

import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
56
import java.io.Serializable;
57 58 59 60 61 62 63 64 65 66
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
 */
67
public class ZFSInstaller extends AdministrativeMonitor implements Serializable {
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
    /**
     * 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() {
K
kohsuke 已提交
91
        if(!System.getProperty("os.name").equals("SunOS") || disabled)
92 93 94 95 96
            // on systems that don't have ZFS, we don't need this monitor
            return false;

        try {
            LibZFS zfs = new LibZFS();
K
kohsuke 已提交
97
            List<ZFSFileSystem> roots = zfs.roots();
98 99 100 101 102 103 104 105 106
            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
K
kohsuke 已提交
107
            ZFSFileSystem pool = roots.get(0);
108 109 110 111 112 113
            prospectiveZfsFileSystemName = computeHudsonFileSystemName(zfs,pool);

            return true;
        } catch (Exception e) {
            LOGGER.log(Level.WARNING, "Failed to detect whether Hudson is on ZFS",e);
            return false;
114
        } catch (LinkageError e) {
K
kohsuke 已提交
115 116
            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);
117
            return false;
118 119 120 121 122 123 124 125 126 127 128 129 130
        }
    }

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

        if(req.hasParameter("n")) {
            // we'll shut up
            disable(true);
K
bug fix  
kohsuke 已提交
131
            rsp.sendRedirect2(req.getContextPath()+"/manage");
132 133 134 135 136 137
            return;
        }

        rsp.sendRedirect2("confirm");
    }

138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
    /**
     * 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
153 154
        final int uid = LIBC.geteuid();
        final int gid = LIBC.getegid();
155 156 157 158 159 160 161 162 163
        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
164
        return SU.execute(listener, rootUsername, rootPassword, new Callable<String,IOException>() {
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
            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);

180 181 182 183 184 185 186 187
                // 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();

188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
                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();
            }
205
        });
206 207
    }

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

216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
        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

242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
        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);
                    }

261
                    // re-exec with the system property to indicate where to migrate the data to.
262
                    // the 2nd phase is implemented in the migrate method.
263
                    JavaVMArguments args = JavaVMArguments.current();
264
                    args.setSystemProperty(ZFSInstaller.class.getName()+".migrate",datasetName);
265 266 267 268 269 270 271 272 273 274
                    Daemon.selfExec(args);
                } catch (InterruptedException e) {
                    LOGGER.log(Level.SEVERE, "Restart failed",e);
                } catch (IOException e) {
                    LOGGER.log(Level.SEVERE, "Restart failed",e);
                }
            }
        }.start();
    }

275 276
    @Extension
    public static AdministrativeMonitor init() {
277 278
        String migrationTarget = System.getProperty(ZFSInstaller.class.getName() + ".migrate");
        if(migrationTarget!=null) {
279 280 281
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            StreamTaskListener listener = new StreamTaskListener(new ForkOutputStream(System.out, out));
            try {
282
                if(migrate(listener,migrationTarget)) {
283
                    // completed successfully
284
                    return new MigrationCompleteNotice();
285
                }
K
kohsuke 已提交
286 287
            } catch (Exception e) {
                // if we let any exception from here, it will prevent Hudson from starting.
288 289 290
                e.printStackTrace(listener.error("Migration failed"));
            }
            // migration failed
291
            return new MigrationFailedNotice(out);
292 293
        }

K
kohsuke 已提交
294 295 296
        // install the monitor if applicable
        ZFSInstaller zi = new ZFSInstaller();
        if(zi.isActivated())
297 298 299
            return zi;

        return null;
300 301 302 303 304 305 306 307 308
    }

    /**
     * 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.
309 310
     * @param target
     *      Dataset to move the data to.
311 312 313
     * @return
     *      false if a migration failed.
     */
314
    private static boolean migrate(TaskListener listener, String target) throws IOException, InterruptedException {
315 316 317 318 319 320 321 322 323 324 325 326 327 328
        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
329 330
        out.println("Opening "+target);
        ZFSFileSystem hudson = zfs.open(target, ZFSFileSystem.class);
331
        hudson.setMountPoint(tmpDir);
K
kohsuke 已提交
332
        hudson.setProperty("hudson:managed-by","hudson"); // mark this file system as "managed by Hudson"
333 334 335 336 337 338 339 340 341 342
        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
343
        out.println("Unmounting "+target);
344 345 346 347 348
        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);
K
kohsuke 已提交
349 350
        if(backup.exists())
            Util.deleteRecursive(backup);
351 352 353 354 355 356 357 358 359
        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);

360
        out.println("Mounting "+target);
361 362 363
        hudson.setMountPoint(home);
        hudson.mount();

364
        out.println("Sharing "+target);
365 366 367 368 369 370 371
        try {
            hudson.setProperty("sharesmb","on");
            hudson.setProperty("sharenfs","on");
            hudson.share();
        } catch (ZFSException e) {
            listener.error("Failed to share the file systems: "+e.getCode());
        }
K
kohsuke 已提交
372

373 374 375
        // delete back up
        out.println("Deleting "+backup);
        if(system(new File("/"),listener,"/usr/bin/rm","-rf",backup.getAbsolutePath())!=0) {
K
bug fix  
kohsuke 已提交
376
            out.println("Failed to delete "+backup.getAbsolutePath());
377 378 379 380 381 382 383 384
            return false;
        }

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

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

K
kohsuke 已提交
388 389 390
    private static String computeHudsonFileSystemName(LibZFS zfs, ZFSFileSystem top) {
        if(!zfs.exists(top.getName()+"/hudson"))
            return top.getName()+"/hudson";
391
        for( int i=2; ; i++ ) {
K
kohsuke 已提交
392
            String name = top.getName() + "/hudson" + i;
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426
            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());
K
kohsuke 已提交
427 428 429 430 431

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