ZFSInstaller.java 16.2 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
import hudson.model.AdministrativeMonitor;
33
import jenkins.model.Jenkins;
34
import hudson.model.TaskListener;
35
import hudson.util.ForkOutputStream;
36 37 38
import hudson.util.HudsonIsRestarting;
import hudson.util.StreamTaskListener;
import static hudson.util.jna.GNUCLibrary.*;
39

40
import jenkins.security.MasterToSlaveCallable;
41 42 43
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.jvnet.libpam.impl.CLibrary.passwd;
import org.jvnet.solaris.libzfs.ACLBuilder;
44
import org.jvnet.solaris.libzfs.LibZFS;
45
import org.jvnet.solaris.libzfs.ZFSException;
46
import org.jvnet.solaris.libzfs.ZFSFileSystem;
47
import org.jvnet.solaris.libzfs.ErrorCode;
48 49 50
import org.jvnet.solaris.mount.MountFlags;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
51
import org.kohsuke.stapler.QueryParameter;
52
import org.kohsuke.stapler.HttpResponse;
53
import org.kohsuke.stapler.HttpResponses;
54
import org.kohsuke.stapler.HttpRedirect;
55
import org.kohsuke.stapler.interceptor.RequirePOST;
56 57 58 59 60

import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
61
import java.io.Serializable;
62 63 64 65 66
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
K
Kohsuke Kawaguchi 已提交
67
 * Encourages the user to migrate JENKINS_HOME on a ZFS file system.
68 69 70 71
 *
 * @author Kohsuke Kawaguchi
 * @since 1.283
 */
72
public class ZFSInstaller extends AdministrativeMonitor implements Serializable {
73 74
    private static final long serialVersionUID = 1018007614648118323L;

75
    /**
K
Kohsuke Kawaguchi 已提交
76
     * True if $JENKINS_HOME is a ZFS file system by itself.
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
     */
    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 已提交
98
        if(!System.getProperty("os.name").equals("SunOS") || disabled)
99 100 101 102 103
            // on systems that don't have ZFS, we don't need this monitor
            return false;

        try {
            LibZFS zfs = new LibZFS();
K
kohsuke 已提交
104
            List<ZFSFileSystem> roots = zfs.roots();
105 106 107 108
            if(roots.isEmpty())
                return false;       // no active ZFS pool

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

            // decide what file system we'll create
K
kohsuke 已提交
114
            ZFSFileSystem pool = roots.get(0);
115 116 117 118 119 120
            prospectiveZfsFileSystemName = computeHudsonFileSystemName(zfs,pool);

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

    /**
     * Called from the management screen.
     */
131
    @RequirePOST
132
    public HttpResponse doAct(StaplerRequest req) throws ServletException, IOException {
133
        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
134 135 136 137

        if(req.hasParameter("n")) {
            // we'll shut up
            disable(true);
138
            return HttpResponses.redirectViaContextPath("/manage");
139 140
        }

141
        return new HttpRedirect("confirm");
142 143
    }

144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
    /**
     * 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
159 160
        final int uid = LIBC.geteuid();
        final int gid = LIBC.getegid();
161 162 163 164 165
        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;

166
        final File home = Jenkins.getInstance().getRootDir();
167 168 169

        // this is the actual creation of the file system.
        // return true indicating a success
170
        return SU.execute(listener, rootUsername, rootPassword, new MasterToSlaveCallable<String,IOException>() {
171 172
            private static final long serialVersionUID = 7731167233498214301L;

173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
            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);

188 189 190 191 192 193 194 195
                // 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();

196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
                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();
            }
213
        });
214 215
    }

216 217 218
    /**
     * Called from the confirmation screen to actually initiate the migration.
     */
219
    @RequirePOST
220
    public void doStart(StaplerRequest req, StaplerResponse rsp, @QueryParameter String username, @QueryParameter String password) throws ServletException, IOException {
221 222
        Jenkins hudson = Jenkins.getInstance();
        hudson.checkPermission(Jenkins.ADMINISTER);
223

224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
        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

250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
        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);
                    }

269
                    // re-exec with the system property to indicate where to migrate the data to.
270
                    // the 2nd phase is implemented in the migrate method.
271
                    JavaVMArguments args = JavaVMArguments.current();
272
                    args.setSystemProperty(ZFSInstaller.class.getName()+".migrate",datasetName);
273 274 275 276 277 278 279 280 281 282
                    Daemon.selfExec(args);
                } catch (InterruptedException e) {
                    LOGGER.log(Level.SEVERE, "Restart failed",e);
                } catch (IOException e) {
                    LOGGER.log(Level.SEVERE, "Restart failed",e);
                }
            }
        }.start();
    }

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

K
kohsuke 已提交
302 303 304
        // install the monitor if applicable
        ZFSInstaller zi = new ZFSInstaller();
        if(zi.isActivated())
305 306 307
            return zi;

        return null;
308 309 310
    }

    /**
K
Kohsuke Kawaguchi 已提交
311
     * Migrates $JENKINS_HOME to a new ZFS file system.
312 313 314 315 316
     *
     * TODO: do this in a separate JVM to elevate the privilege.
     *
     * @param listener
     *      Log of migration goes here.
317 318
     * @param target
     *      Dataset to move the data to.
319 320 321
     * @return
     *      false if a migration failed.
     */
322
    private static boolean migrate(TaskListener listener, String target) throws IOException, InterruptedException {
323 324
        PrintStream out = listener.getLogger();

325
        File home = Jenkins.getInstance().getRootDir();
326 327 328 329 330 331 332 333 334 335 336
        // 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
337 338
        out.println("Opening "+target);
        ZFSFileSystem hudson = zfs.open(target, ZFSFileSystem.class);
339
        hudson.setMountPoint(tmpDir);
K
kohsuke 已提交
340
        hudson.setProperty("hudson:managed-by","hudson"); // mark this file system as "managed by Hudson"
341 342 343 344 345 346 347 348 349 350
        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
351
        out.println("Unmounting "+target);
352 353 354 355 356
        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 已提交
357 358
        if(backup.exists())
            Util.deleteRecursive(backup);
359 360 361 362 363 364 365 366 367
        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);

368
        out.println("Mounting "+target);
369 370 371
        hudson.setMountPoint(home);
        hudson.mount();

372
        out.println("Sharing "+target);
373 374 375 376 377 378 379
        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 已提交
380

381 382 383
        // delete back up
        out.println("Deleting "+backup);
        if(system(new File("/"),listener,"/usr/bin/rm","-rf",backup.getAbsolutePath())!=0) {
K
bug fix  
kohsuke 已提交
384
            out.println("Failed to delete "+backup.getAbsolutePath());
385 386 387 388 389 390 391 392
            return false;
        }

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

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

K
kohsuke 已提交
396 397 398
    private static String computeHudsonFileSystemName(LibZFS zfs, ZFSFileSystem top) {
        if(!zfs.exists(top.getName()+"/hudson"))
            return top.getName()+"/hudson";
399
        for( int i=2; ; i++ ) {
K
kohsuke 已提交
400
            String name = top.getName() + "/hudson" + i;
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 427 428 429 430 431 432 433 434
            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 已提交
435 436 437 438 439

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