diff --git a/core/pom.xml b/core/pom.xml index a04017a393b0141956b216dc02aebdc5d4c81251..d399676598079958c7c88f2e75426f433bc34e9e 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -635,7 +635,12 @@ THE SOFTWARE. org.jvnet.libzfs libzfs - 0.2 + 0.3 + + + com.sun.solaris + embedded_su4j + 1.0 diff --git a/core/src/main/java/hudson/Launcher.java b/core/src/main/java/hudson/Launcher.java index ffb47b4018e36dfa7d6bad07fd7150907c68cd26..a89c6258317e979018c647a7eeadc380ec113911 100644 --- a/core/src/main/java/hudson/Launcher.java +++ b/core/src/main/java/hudson/Launcher.java @@ -345,15 +345,26 @@ public abstract class Launcher { public Channel launchChannel(String[] cmd, OutputStream out, FilePath workDir, Map envVars) throws IOException { printCommandLine(cmd, workDir); + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.directory(toFile(workDir)); + + return launchChannel(out, pb); + } + + /** + * @param out + * Where the stderr from the launched process will be sent. + */ + public Channel launchChannel(OutputStream out, ProcessBuilder pb) throws IOException { final EnvVars cookie = ProcessTreeKiller.createCookie(); - EnvVars map = Launcher.inherit(envVars); - map.putAll(cookie); - final Process proc = Runtime.getRuntime().exec(cmd, Util.mapToEnv(map), toFile(workDir)); + pb.environment().putAll(cookie); + + final Process proc = pb.start(); - final Thread t2 = new StreamCopyThread(Arrays.asList(cmd)+": stderr copier", proc.getErrorStream(), out); + final Thread t2 = new StreamCopyThread(pb.command()+": stderr copier", proc.getErrorStream(), out); t2.start(); - return new Channel("locally launched channel on "+ Arrays.toString(cmd), + return new Channel("locally launched channel on "+ pb.command(), Computer.threadPoolForRemoting, proc.getInputStream(), proc.getOutputStream(), out) { /** diff --git a/core/src/main/java/hudson/lifecycle/ZFSInstaller.java b/core/src/main/java/hudson/lifecycle/ZFSInstaller.java index 958bbfc64a9a0e45e945acdbb0cf4218aa7eb18b..ce9ec4b07eba3e316f3b8cca4bd5afbda1da6f9f 100644 --- a/core/src/main/java/hudson/lifecycle/ZFSInstaller.java +++ b/core/src/main/java/hudson/lifecycle/ZFSInstaller.java @@ -25,30 +25,41 @@ package hudson.lifecycle; import com.sun.akuma.Daemon; import com.sun.akuma.JavaVMArguments; +import com.sun.solaris.EmbeddedSu; import hudson.FilePath; import hudson.Launcher.LocalLauncher; import hudson.Util; import hudson.model.AdministrativeMonitor; +import hudson.model.Computer; import hudson.model.Hudson; import hudson.model.TaskListener; +import hudson.remoting.Callable; +import hudson.remoting.Channel; +import hudson.remoting.Launcher; +import hudson.remoting.Which; +import hudson.util.ForkOutputStream; import hudson.util.HudsonIsRestarting; import hudson.util.StreamTaskListener; -import hudson.util.ForkOutputStream; 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.ZFSPool; -import org.jvnet.solaris.libzfs.ZFSType; -import org.jvnet.solaris.libzfs.ZFSException; +import org.jvnet.solaris.libzfs.ErrorCode; import org.jvnet.solaris.mount.MountFlags; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; -import org.apache.commons.io.output.ByteArrayOutputStream; +import org.kohsuke.stapler.QueryParameter; import javax.servlet.ServletException; import java.io.File; import java.io.IOException; import java.io.PrintStream; +import java.io.Serializable; +import java.util.Collections; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -59,7 +70,7 @@ import java.util.logging.Logger; * @author Kohsuke Kawaguchi * @since 1.283 */ -public class ZFSInstaller extends AdministrativeMonitor { +public class ZFSInstaller extends AdministrativeMonitor implements Serializable { /** * True if $HUDSON_HOME is a ZFS file system by itself. */ @@ -126,14 +137,135 @@ public class ZFSInstaller extends AdministrativeMonitor { rsp.sendRedirect2("confirm"); } + /** + * Creates a ZFS file system to migrate the data to. + * + *

+ * This has to be done while we still have an interactive access with the user, since it involves the password. + * + *

+ * 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 + int uid = LIBC.geteuid(); + 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 + Callable task = new Callable() { + 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); + + 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(); + } + }; + + + // if we are the root user already, we can just do it here. + // if that fails, no amount of pfexec and embedded_sudo would do. + if(uid==0) + return task.call(); + + String javaExe = System.getProperty("java.home") + "/bin/java"; + String slaveJar = Which.jarFile(Launcher.class).getAbsolutePath(); + + // otherwise first attempt pfexec, as that doesn't require password + Channel channel; + Process proc=null; + + if(rootPassword==null) { + // try pfexec, in the hope that the user has the permission + channel = new LocalLauncher(listener).launchChannel( + new String[]{"/usr/bin/pfexec", javaExe, "-jar", slaveJar}, + listener.getLogger(), null, Collections.emptyMap()); + } else { + // try sudo with the given password + ProcessBuilder pb = new ProcessBuilder(javaExe,"-jar",slaveJar); + proc = EmbeddedSu.startWithSu(rootUsername, rootPassword, pb); + channel = new Channel("zfs migration thread", Computer.threadPoolForRemoting, + proc.getInputStream(), proc.getOutputStream(), listener.getLogger()); + } + + try { + return channel.call(task); + } finally { + channel.close(); + if(proc!=null) + proc.destroy(); + } + } + /** * Called from the confirmation screen to actually initiate the migration. */ - public void doStart(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException { - requirePOST(); + 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"); @@ -153,8 +285,10 @@ public class ZFSInstaller extends AdministrativeMonitor { 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 starts in the init method. JavaVMArguments args = JavaVMArguments.current(); - args.setSystemProperty(ZFSInstaller.class.getName(),"migrate"); + args.setSystemProperty(ZFSInstaller.class.getName()+".migrate",datasetName); Daemon.selfExec(args); } catch (InterruptedException e) { LOGGER.log(Level.SEVERE, "Restart failed",e); @@ -169,11 +303,12 @@ public class ZFSInstaller extends AdministrativeMonitor { public static void init() { List monitors = Hudson.getInstance().administrativeMonitors; - if("migrate".equals(System.getProperty(ZFSInstaller.class.getName()))) { + 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)) { + if(migrate(listener,migrationTarget)) { // completed successfully monitors.add(new MigrationCompleteNotice()); return; @@ -199,10 +334,12 @@ public class ZFSInstaller extends AdministrativeMonitor { * * @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) throws IOException, InterruptedException { + private static boolean migrate(TaskListener listener, String target) throws IOException, InterruptedException { PrintStream out = listener.getLogger(); File home = Hudson.getInstance().getRootDir(); @@ -217,9 +354,8 @@ public class ZFSInstaller extends AdministrativeMonitor { File tmpDir = Util.createTempDir(); // mount a new file system to a temporary location - String name = computeHudsonFileSystemName(zfs, zfs.roots().get(0)); - out.println("Creating "+name); - ZFSFileSystem hudson = (ZFSFileSystem)zfs.create(name, ZFSType.FILESYSTEM); + 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(); @@ -232,7 +368,7 @@ public class ZFSInstaller extends AdministrativeMonitor { } // unmount - out.println("Unmounting "+name); + out.println("Unmounting "+target); hudson.unmount(MountFlags.MS_FORCE); // move the original directory to the side @@ -247,11 +383,11 @@ public class ZFSInstaller extends AdministrativeMonitor { if(!home.mkdir()) throw new IOException("Failed to create mount point "+home); - out.println("Mounting "+name); + out.println("Mounting "+target); hudson.setMountPoint(home); hudson.mount(); - out.println("Sharing "+name); + out.println("Sharing "+target); hudson.setProperty("sharesmb","on"); hudson.setProperty("sharenfs","on"); hudson.share(); diff --git a/core/src/main/java/hudson/util/jna/GNUCLibrary.java b/core/src/main/java/hudson/util/jna/GNUCLibrary.java index bdf51671a34010b4c706f03a4fe89d8046f3814e..79db6d75089ab924b6a4b2c54dbb818656720919 100644 --- a/core/src/main/java/hudson/util/jna/GNUCLibrary.java +++ b/core/src/main/java/hudson/util/jna/GNUCLibrary.java @@ -28,6 +28,7 @@ import com.sun.jna.StringArray; import com.sun.jna.Pointer; import com.sun.jna.Native; import com.sun.jna.ptr.IntByReference; +import org.jvnet.libpam.impl.CLibrary.passwd; /** * GNU C library. @@ -51,6 +52,8 @@ public interface GNUCLibrary extends Library { void perror(String msg); String strerror(int errno); + passwd getpwuid(int uid); + int fcntl(int fd, int command); int fcntl(int fd, int command, int flags); diff --git a/core/src/main/resources/hudson/lifecycle/ZFSInstaller/askRootPassword.jelly b/core/src/main/resources/hudson/lifecycle/ZFSInstaller/askRootPassword.jelly new file mode 100644 index 0000000000000000000000000000000000000000..96faf0309eee8ba207b4498836d0dc9e06916d20 --- /dev/null +++ b/core/src/main/resources/hudson/lifecycle/ZFSInstaller/askRootPassword.jelly @@ -0,0 +1,62 @@ + + + + + + + +

+ + ${%Permission Denied} +

+
+ ${%blurb} + + + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/lifecycle/ZFSInstaller/askRootPassword.properties b/core/src/main/resources/hudson/lifecycle/ZFSInstaller/askRootPassword.properties new file mode 100644 index 0000000000000000000000000000000000000000..c8da85692ec5e8f869f74cfb7d701a065cae5944 --- /dev/null +++ b/core/src/main/resources/hudson/lifecycle/ZFSInstaller/askRootPassword.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi +# +# 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. + +blurb=It appears that the current user account lacks necessary permissions to create a ZFS file system. \ + Please provide the username and the password that's capable of doing this, such as root. \ No newline at end of file