package hudson.plugins.pxe; import hudson.Extension; import hudson.Util; import hudson.model.Hudson; import static hudson.util.FormValidation.error; import org.apache.commons.io.IOUtils; import org.jvnet.hudson.tftpd.Data; import org.kohsuke.loopy.FileEntry; import org.kohsuke.loopy.iso9660.ISO9660FileSystem; import org.kohsuke.stapler.DataBoundConstructor; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.URL; import java.net.BindException; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; import java.util.logging.Level; import static java.util.logging.Level.WARNING; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * OpenSolaris boot. * * <h2>References</h2> * <ul> * <li><a href="http://wikis.sun.com/display/OSOLInstall/Automated+Installer+Core+Engine"> * Auto Installer Code Walkthrough</a> * * <li><a href="http://src.opensolaris.org/source/xref/caiman/slim_bp_installgrub/usr/src/cmd/auto-install/"> * Auto Installer source code</a> * * @author Kohsuke Kawaguchi */ public class OpenSolarisBootConfiguration extends IsoBasedBootConfiguration { private transient AIWebServerThread aiServer; @DataBoundConstructor public OpenSolarisBootConfiguration(File iso) throws IOException { super(iso); startAIServer(); } private void startAIServer() { try { aiServer = new AIWebServerThread(); aiServer.start(); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to restart the AI web server",e); } } /** * Is this object still being used? */ private boolean isActive() { return PXE.get().getConfiguration(getId())==this; } protected String getIdSeed() { // try to extract a short name from the release information Pattern p = Pattern.compile("snv_[^ ]+"); Matcher m = p.matcher(getRelease()); if(m.find()) return m.group(0); return "opensolaris"; } @Override protected void shutdown() throws IOException { if(aiServer!=null) // we might have failed to initialize ai server. aiServer.close(); } /** * Chainboot to pxegrub, which in turn knows how to boot Solaris. * * Solaris boot requires $ISADIR variable that we don't know how to set. */ public String getPxeLinuxConfigFragment() throws IOException { // return String.format("LABEL %1$s\n" + // " MENU LABEL %2$s\n" + // " KERNEL pxechain.com\n" + // " APPEND ::%3$s/boot/grub/pxegrub \n", // getId(), getDisplayName(), getId() ); String baseUrl = Hudson.getInstance().getRootUrl(); String host = new URL(baseUrl).getHost(); baseUrl = baseUrl.replace(host,InetAddress.getByName(host).getHostAddress()); String httpIsoImage = String.format("%1$spxe/configuration/%2$s/image", baseUrl,getId()); return String.format("LABEL %1$s\n" + " MENU LABEL %2$s\n" + " KERNEL mboot.c32\n" + " APPEND -solaris %1$s/boot/platform/i86pc/kernel/unix -v -m verbose -B install_media=%3$s,install_boot=%3$s/boot,livemode=text,install_service=dummy,install_svc_address=%4$s:%5$s --- %1$s/boot/boot_archive\n", getId(), getDisplayName(), httpIsoImage, host, aiServer.server.getLocalPort()); } public Object readResolve() { startAIServer(); return this; } @Override public Data tftp(String fileName) throws IOException { // TODO: closing this file system voids FileEntryData. Fix it ISO9660FileSystem fs = new ISO9660FileSystem(iso, false); return new FileEntryData(fs.getRootEntry().grab(fileName)); } @Extension public static class DescriptorImpl extends IsoBasedBootConfigurationDescriptor { public String getDisplayName() { return "OpenSolaris"; } /** * This is like "OpenSolaris 2008.11 svnc_101b_rc2 X86" */ protected String getReleaseInfo(File iso) throws IOException { ISO9660FileSystem fs=null; try { try { fs = new ISO9660FileSystem(iso,false); } catch (IOException e) { LOGGER.log(Level.INFO,iso+" isn't an ISO file?",e); throw error(iso+" doesn't look like an ISO file"); } if(fs.get("/solaris.zlib")==null || fs.get("/jack")==null) throw error(iso+" doesn't look like an OpenSolaris CD image"); FileEntry menu = fs.get("/boot/grub/menu.lst"); if(menu==null) throw error(iso+" doesn't look like an OpenSolaris CD image (no GRUB)"); String menuList = IOUtils.toString(menu.read()); Matcher m = RELEASE.matcher(menuList); if(m.find()) return m.group(1); throw error(iso+" doesn't contain OpenSolaris grub menu"); } finally { if(fs!=null) fs.close(); } } } /** * Starts a micro web server that handles AI web requests. This has to be on a separate TCP port * because the client only knows how to access the /manifests.xml */ private class AIWebServerThread extends Thread { private final ServerSocket server; private boolean closed; public AIWebServerThread() throws IOException { super("OpenSolaris AI webserver for " + iso); setDaemon(true); server = openSocket(); LOGGER.info("OpenSolaris AI server for "+iso+" started on port "+server.getLocalPort()); } public void close() throws IOException { LOGGER.info("Shutting down "+getName()); closed=true; server.close(); } private ServerSocket openSocket() throws IOException { // the port could be anything, but it's often easier if the port doesn't change too much, // so try to stick to the one that we can programmatically infer int preferred = Math.abs(OpenSolarisBootConfiguration.this.getIdSeed().hashCode()) % 40000 + 10000; try { return new ServerSocket(preferred); } catch (BindException e) { // OK, that one wasn't available. pick available one return new ServerSocket(0); } } @Override public void run() { while(isActive()) { final Socket s; try { s = server.accept(); } catch (IOException e) { if(!closed) LOGGER.log(WARNING, "Failed to accept",e); return; // exit } // handle the request in a separate thread new Thread() { @Override public void run() { try { // the goal here is to avoid infinite blocking, so set a long time out. s.setSoTimeout(10*60*1000); BufferedReader r = new BufferedReader(new InputStreamReader(s.getInputStream())); PrintStream out = new PrintStream(s.getOutputStream()); String request = r.readLine(); if(request.startsWith("GET /manifest.xml")) { out.println("HTTP/1.0 200 OK"); out.println("Content-Type: text/xml"); out.println(""); out.println("<CriteriaList>"); out.println(" <Version Number=\"0.5\"/>"); out.println("</CriteriaList>"); } else if(request.startsWith("POST /manifest.xml")) { out.println("HTTP/1.0 200 OK"); out.println("Content-Type: text/xml"); out.println(""); String template = IOUtils.toString(getResourceAsStream("ai.xml")); Map<String,String> props = new HashMap<String, String>(); props.put("userName","jack"); props.put("rootPassword",Crypt.cryptMD5("abcdefgh","opensolaris")); props.put("timeZone", TimeZone.getDefault().getID()); out.println(Util.replaceMacro(template,props)); } else { out.println("HTTP/1.0 404 Not Found"); out.println("Content-Type: text/html"); out.println(""); out.println("<html><body>This server only knows how to handle /manifest.xml</body></html>"); } // close the write side out.flush(); s.shutdownOutput(); IOUtils.toString(r); // drain the input s.shutdownInput(); } catch(IOException e) { LOGGER.log(WARNING, "Failed to serve a request from AI web server",e); } finally { try { s.close(); } catch (IOException e) { LOGGER.log(WARNING, "Failed to close a socket in AI web server",e); } } } }.start(); } LOGGER.fine(" AI web server thread exiting"); } } private static final Pattern RELEASE = Pattern.compile("title (.+)\n"); private static final Logger LOGGER = Logger.getLogger(OpenSolarisBootConfiguration.class.getName()); }