package hudson.plugins.pxe;
import hudson.BulkChange;
import hudson.Extension;
import hudson.Util;
import hudson.XmlFile;
import hudson.tasks.Mailer;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.Descriptor.FormException;
import hudson.model.Hudson;
import hudson.model.ManagementLink;
import hudson.model.Saveable;
import hudson.model.TaskListener;
import hudson.os.SU;
import hudson.remoting.VirtualChannel;
import hudson.util.DescribableList;
import hudson.util.FormValidation;
import hudson.util.Secret;
import hudson.util.StreamTaskListener;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.framework.io.LargeText;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Collections;
import java.util.logging.Logger;
import java.text.ParseException;
/**
* This object is bound to "/pxe" and handles all the UI work.
*
* @author Kohsuke Kawaguchi
*/
@Extension
public class PXE extends ManagementLink implements StaplerProxy, Describable<PXE>, Saveable {
private String rootUserName;
private Secret rootPassword;
private final DescribableList<BootConfiguration, BootConfigurationDescriptor> bootConfigurations = new DescribableList<BootConfiguration, BootConfigurationDescriptor>(this);
/**
* Numberic IP address of the TFTP server, like "1.2.3.4"
*/
private String tftpAddress;
// running state
private transient VirtualChannel channel;
private transient DaemonService daemonService;
/**
* If true, only respond to known MAC addresses that the administrator approved.
*/
private boolean respondSelectively;
/**
* Mac addresses that we'll respond
*/
private transient Set<MacAddress> approvedMacAddresses = new HashSet<MacAddress>();
/**
* Mac addresses that we've seen sending out DHCP request. This is a set but it's chronologically ordered.
*/
private transient LinkedHashSet<MacAddress> discoveredMacAddresses = new LinkedHashSet<MacAddress>();
public PXE() throws IOException, InterruptedException {
load();
assignIDs();
restartPXE();
}
public String getIconFileName() {
return "orange-square.gif";
}
@Override
public String getDescription() {
return "Simplify the slave installation by network installing them (AKA PXE boot)";
}
public String getUrlName() {
return "pxe";
}
public String getDisplayName() {
return "Network Slave Installation Management";
}
public String getRootUserName() {
return rootUserName;
}
public Secret getRootPassword() {
return rootPassword;
}
public DescribableList<BootConfiguration, BootConfigurationDescriptor> getBootConfigurations() {
return bootConfigurations;
}
public void setTftpAddress(String address) throws IOException {
this.tftpAddress = address;
save();
}
public String getTftpAddress() {
return tftpAddress;
}
public boolean isRespondSelectively() {
return respondSelectively;
}
public boolean isRespondToAll() {
return !respondSelectively;
}
public void setRespondSelectively(boolean respondSelectively) throws IOException {
this.respondSelectively = respondSelectively;
save();
}
public Set<MacAddress> getAddressesThatRequireApproval() {
discoveredMacAddresses.removeAll(approvedMacAddresses);
return Collections.unmodifiableSet(discoveredMacAddresses);
}
public VirtualChannel getChannel() {
return channel;
}
public DaemonService getDaemonService() {
return daemonService;
}
public File getLogFile() {
return new File(Hudson.getInstance().getRootDir(),"pxe.log");
}
public synchronized void restartPXE() throws IOException, InterruptedException {
if(Hudson.getInstance().getRootUrl()==null) {
LOGGER.warning("Not starting TFTP/ProxyDHCP because Hudson Root URL is not configured");
return;
}
if(tftpAddress==null) {
LOGGER.warning("Not starting TFTP/ProxyDHCP service due to incomplete configuration");
return;
}
if(channel!=null) {
LOGGER.info("Stopping TFTP/ProxyDHCP service");
channel.close();
channel.join(3000);
}
LOGGER.info("Starting TFTP/ProxyDHCP service");
TaskListener listener = new StreamTaskListener(getLogFile());
channel = SU.start(listener, rootUserName, rootPassword == null ? null : rootPassword.toString());
// export explicitly, or else it'll be unexported upon return
daemonService = channel.call(new PXEBootProcess(
new PathResolverImpl().export(channel),
channel.export(DHCPPacketFilter.class, createDHCPPacketFilter()),
tftpAddress));
}
private DHCPPacketFilter createDHCPPacketFilter() {
return new DHCPPacketFilter() {
public boolean shallWeRespond(byte[] address) {
if(isRespondToAll()) return true;
MacAddress a = new MacAddress(address);
if (approvedMacAddresses.contains(a)) return true;
// force the insertion into tail
discoveredMacAddresses.remove(a);
discoveredMacAddresses.add(a);
return false;
}
};
}
public void setRootAccount(String userName, Secret password) throws IOException {
this.rootUserName = Util.fixEmptyAndTrim(userName);
this.rootPassword = password;
save();
}
/**
* Obtains the status of the daemon.
*/
public FormValidation getDaemonStatus() {
if(Mailer.descriptor().getUrl()==null)
return FormValidation.warningWithMarkup("<a href='../configure'>Hudson Root URL is not configured</a>.");
DaemonService ds = getDaemonService();
if(ds==null)
return FormValidation.warningWithMarkup("PXE service is not yet started. <a href='console'>Check console for the status</a>");
if(!ds.isDHCPProxyAlive())
return FormValidation.errorWithMarkup("DHCP proxy service is failing. <a href='console'>Check console for the status</a>");
if(!ds.isTFTPAlive())
return FormValidation.error("TFTP service is failing");
return FormValidation.ok();
}
/**
* All registered descriptors exposed for UI
*/
public Collection<BootConfigurationDescriptor> getDescriptors() {
return BootConfiguration.all();
}
/**
* Looks up {@link BootConfiguration} by its {@linkplain BootConfiguration#getId() id}.
*
* Primarily used to bind them to URL.
*/
public BootConfiguration getConfiguration(String id) {
for (BootConfiguration config : getBootConfigurations())
if(config.getId().equals(id))
return config;
return null;
}
/**
* Access to this object requires the admin permission.
*/
public Object getTarget() {
Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
return this;
}
public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException, InterruptedException {
JSONObject form = req.getSubmittedForm();
// persist the setting
BulkChange bc = new BulkChange(this);
try {
setRootAccount(form.getString("rootUserName"),Secret.fromString(form.getString("rootPassword")));
if(form.has("tftpAddress"))
setTftpAddress(form.getString("tftpAddress"));
else
setTftpAddress(getNICs().get(0).adrs.getHostAddress());
setRespondSelectively(!form.has("respondToAll"));
for (BootConfiguration c : bootConfigurations)
c.shutdown();
bootConfigurations.rebuildHetero(req,form,getDescriptors(),"configuration");
assignIDs();
} catch (FormException e) {
throw new ServletException(e);
} finally {
bc.commit();
}
restartPXE();
rsp.sendRedirect(".");
}
public void doDoApprove(StaplerRequest req, StaplerResponse rsp) throws ServletException, ParseException, IOException {
JSONObject form = req.getSubmittedForm();
if (form.has("mac")) {
JSONObject j = form.getJSONObject("mac");
for (String key : (Set<String>) j.keySet()) {
if (j.getBoolean(key))
approvedMacAddresses.add(new MacAddress(key));
}
}
String additional = (String)form.get("additional");
if (additional!=null) {
for (String key : additional.split("\n")) {
key = key.trim();
if (key.length()>0)
approvedMacAddresses.add(new MacAddress(key));
}
}
if (req.hasParameter("reset"))
approvedMacAddresses.clear();
rsp.sendRedirect(".");
}
private void assignIDs() {
// recompute IDs
DescribableList<BootConfiguration, BootConfigurationDescriptor> all = getBootConfigurations();
OUTER:
for (BootConfiguration a : all) {
String seed = a.getIdSeed();
for( BootConfiguration b : all) {
if(b==a) continue;
if(b.getIdSeed().equals(seed)) {
// conflict. resolve by adding index
int index=1;
for (BootConfiguration c : all) {
if(c==a) {
a.id=seed+"_"+index;
continue OUTER;
}
if(c.getIdSeed().equals(seed))
index++;
}
throw new AssertionError();
}
}
// no conflict
a.id=seed;
}
}
/**
* Handles incremental log output.
*/
public void doProgressiveLog( StaplerRequest req, StaplerResponse rsp) throws IOException {
new LargeText(getLogFile(),false).doProgressText(req,rsp);
}
public DescriptorImpl getDescriptor() {
return Hudson.getInstance().getDescriptorByType(DescriptorImpl.class);
}
/**
* Descriptor is only used for UI form bindings
*/
@Extension
public static final class DescriptorImpl extends Descriptor<PXE> {
public String getDisplayName() {
return null; // unused
}
}
public static final class NIC {
public final NetworkInterface ni;
public final Inet4Address adrs;
public NIC(NetworkInterface ni, Inet4Address adrs) {
this.ni = ni;
this.adrs = adrs;
}
public String getName() {
String n = ni.getDisplayName();
if(n==null) n=ni.getName();
return String.format("%s (%s)",
adrs.getHostAddress(),n);
}
}
/**
* Because DHCP proxy doesn't know which interface the DHCP request was received,
* it cannot determine by itself what IP address the PXE client can use to reach us.
*
* <p>
* This method lists all the interfaces
*/
public List<NIC> getNICs() throws SocketException {
List<NIC> r = new ArrayList<NIC>();
Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();
while (e.hasMoreElements()) {
NetworkInterface ni = e.nextElement();
// require JDK6
// if(ni.isLoopback()) continue;
// if(ni.isPointToPoint()) continue;
Enumeration<InetAddress> adrs = ni.getInetAddresses();
while (adrs.hasMoreElements()) {
InetAddress a = adrs.nextElement();
if(a.isLoopbackAddress())
continue;
if (a instanceof Inet4Address)
r.add(new NIC(ni,(Inet4Address)a));
}
}
return r;
}
protected void load() throws IOException {
XmlFile xml = getConfigXml();
if(xml.exists())
xml.unmarshal(this);
}
public void save() throws IOException {
if(BulkChange.contains(this)) return;
getConfigXml().write(this);
}
protected XmlFile getConfigXml() {
return new XmlFile(Hudson.XSTREAM,
new File(Hudson.getInstance().getRootDir(),"pxe.xml"));
}
public static PXE get() {
return ManagementLink.all().get(PXE.class);
}
private static final Logger LOGGER = Logger.getLogger(PXE.class.getName());
/**
* If for some reason the default root URL won't do --- for example, maybe you need to address the server by its IP.
*/
public static String ROOT_URL = System.getProperty(PXE.class.getName()+".rootURL");
}