Created
April 4, 2015 14:14
-
-
Save tbl3rd/32d7fcd902a004f3e632 to your computer and use it in GitHub Desktop.
manage private LAN addresses
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package tbl3rd.provider; | |
| import tbl3rd.provider.Api_2015_04_01; | |
| import tbl3rd.provider.Api_2015_04_01.NetworkInformation; | |
| import java.io.BufferedWriter; | |
| import java.io.File; | |
| import java.io.FileOutputStream; | |
| import java.io.IOException; | |
| import java.io.OutputStreamWriter; | |
| import java.net.Inet4Address; | |
| import java.security.DigestOutputStream; | |
| import java.security.MessageDigest; | |
| import java.security.NoSuchAlgorithmException; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import java.util.logging.Logger; | |
| /** | |
| * One of these for a worker. | |
| * | |
| * host worker-r001-c05-n02 { | |
| * hardware ethernet 04:7D:7B:62:91:54; | |
| * fixed-address worker-r001-c05-n02; | |
| * filename "http://172.20.4.254:9000/bootscripts/.../04:7D:7B:62:91:54"; | |
| * } | |
| * | |
| * Or one of these for a switch. | |
| * | |
| * host datasw3 { | |
| * hardware ethernet 04:7d:7b:07:1e:22; | |
| * fixed-address 172.20.4.243; | |
| * } | |
| */ | |
| final class EtcDhcpHostEntry | |
| { | |
| static final String workerFormat = | |
| "\n" + | |
| " host %s {\n" + | |
| " hardware ethernet %s;\n" + | |
| " fixed-address %s;\n" + | |
| " filename \"%s\";\n" + | |
| " }\n"; | |
| static final String bmcFormat = | |
| "\n" + | |
| " host %s {\n" + | |
| " hardware ethernet %s;\n" + | |
| " fixed-address %s;\n" + | |
| " }\n"; | |
| static final String switchFormat = | |
| "host %s {\n" + | |
| " hardware ethernet %s;\n" + | |
| " fixed-address %s;\n" + | |
| "}\n"; | |
| /** | |
| * This host's domain name. | |
| */ | |
| final String hostName; | |
| /** | |
| * This host's IPv4 address. | |
| */ | |
| final Inet4Address ipAddress; | |
| /** | |
| * This host's Ethernet address. | |
| */ | |
| final String ethernetAddress; | |
| /** | |
| * Null or this host's boot URL or "-bmc" for the BMC subnet. | |
| */ | |
| final String bootUrl; | |
| @Override | |
| public String toString() | |
| { | |
| if (bootUrl == null) { | |
| return String.format( | |
| switchFormat, hostName, ethernetAddress, | |
| ipAddress.getHostAddress()); | |
| } else if (bootUrl == "-bmc") { | |
| return String.format( | |
| bmcFormat, hostName, ethernetAddress, hostName); | |
| } else { | |
| return String.format( | |
| workerFormat, hostName, ethernetAddress, hostName, bootUrl); | |
| } | |
| } | |
| /** | |
| * An entry for hostName with ipAddress and ethernetAddress. This | |
| * is a global switch entry if bootUrl is null. Otherwise it is a | |
| * worker entry in some subnet. If bootUrl is "-bmc", the worker | |
| * name should also end in "-bmc" because the entry is for the BMC | |
| * subnet. | |
| */ | |
| EtcDhcpHostEntry( | |
| String hostName, | |
| Inet4Address ipAddress, | |
| String ethernetAddress) | |
| { | |
| this.hostName = hostName; | |
| this.ipAddress = ipAddress; | |
| this.ethernetAddress = ethernetAddress; | |
| this.bootUrl = null; | |
| } | |
| EtcDhcpHostEntry( | |
| String hostName, | |
| Inet4Address ipAddress, | |
| String ethernetAddress, | |
| String bootUrl) | |
| { | |
| this.hostName = hostName; | |
| this.ipAddress = ipAddress; | |
| this.ethernetAddress = ethernetAddress; | |
| this.bootUrl = bootUrl; | |
| } | |
| } | |
| /** | |
| * The contents of an /etc/dhcp/dhcpd.conf file. | |
| */ | |
| final class EtcDhcpDhcpdConf | |
| { | |
| private final static Logger logger = | |
| Logger.getLogger("tbl3rd.provider"); | |
| private static final String pathName = | |
| System.getProperty( | |
| EtcDhcpDhcpdConf.class.getName() + ".pathName", | |
| "/etc/dhcp/dhcpd.conf"); | |
| private static final String serviceName = | |
| System.getProperty( | |
| EtcDhcpDhcpdConf.class.getName() + ".serviceName", | |
| "/sbin/service"); | |
| private static final String digestAlgorithm = | |
| System.getProperty( | |
| EtcDhcpDhcpdConf.class.getName() + ".digestAlgorithm", | |
| "SHA-1"); | |
| private static final String top = | |
| "# THIS FILE IS MACHINE GENERATED, DO NOT EDIT\n" + | |
| "# DHCP Server Configuration file.\n" + | |
| "# see /usr/share/doc/dhcp*/dhcpd.conf.sample\n" + | |
| "# see 'man 5 dhcpd.conf'\n" + | |
| "\n" + | |
| "# Listen only to me. Forswear any other lease.\n" + | |
| "authoritative;\n" + | |
| "\n" + | |
| "# Lease for 1-2 hours.\n" + | |
| "default-lease-time 3600;\n" + | |
| "max-lease-time 7200;\n" + | |
| "get-lease-hostnames true;\n" + | |
| "\n" + | |
| "# Boot only the client hosts listed in this file.\n" + | |
| "boot-unknown-clients false;\n" + | |
| "\n" + | |
| "# Do not update DNS when a client confirms its lease.\n" + | |
| "ddns-update-style none;\n" + | |
| "\n" + | |
| "# Send DHCP logs to local5.\n" + | |
| "log-facility local5;\n" + | |
| "\n"; | |
| private final ConfigStore configStore; | |
| private final EtcHosts etcHosts; | |
| private final String domainName; | |
| private final String[] domainNameServers; | |
| private final boolean noVlansHack; | |
| private final EtcDhcpHostEntry[] switches; | |
| private final EtcDhcpHostEntry[] bmcs; | |
| private final EtcDhcpHostEntry[] workers; | |
| /** | |
| * Return the global parameters. | |
| */ | |
| private final String globals() | |
| { | |
| final StringBuilder result = new StringBuilder( | |
| top + | |
| "option domain-name \"" + domainName + "\";\n" + | |
| "option domain-name-servers "); | |
| String separator = ""; | |
| for (String s : domainNameServers) { | |
| result.append(separator).append(s); | |
| separator = ", "; | |
| } | |
| result.append(";\n\n"); | |
| return result.toString(); | |
| } | |
| /** | |
| * Throw an exception if there is some kind of problem. | |
| */ | |
| private static final void maybeThrowNotEnoughAddresses( | |
| EtcHosts eh, int bmcCount, int dataCount) | |
| throws ApiException | |
| { | |
| final int bmcRequired = bmcCount + | |
| EtcHosts.reserveCount + eh.bmcWorkers.size(); | |
| final int dataRequired = dataCount + | |
| EtcHosts.reserveCount + eh.dataWorkers.size(); | |
| final boolean neBmc = bmcRequired > eh.bmcBlock.hostCount(); | |
| final boolean neData = dataRequired > eh.dataBlock.hostCount(); | |
| if (!(neBmc || neData)) return; | |
| final StringBuilder sb = new StringBuilder( | |
| String.format("Not enough addresses for %s file.", pathName)); | |
| if (neBmc) { | |
| sb.append("\n").append( | |
| String.format("Need %d BMC addresses, but %d provided.", | |
| bmcRequired, eh.bmcBlock.hostCount())); | |
| } | |
| if (neData) { | |
| sb.append("\n").append( | |
| String.format("Need %d data addresses, but %d provided.", | |
| dataRequired, eh.dataBlock.hostCount())); | |
| } | |
| throw new ApiException( | |
| ManagementApi_2012_05_15.ResultCode.BAD_REQUEST, | |
| sb.toString()); | |
| } | |
| /** | |
| * Return switch entries for switches using addresses from eh. | |
| */ | |
| private static EtcDhcpHostEntry[] addSwitches( | |
| EtcHosts eh, ConfigData.Switches switches) | |
| throws ApiException, IOException | |
| { | |
| final int bmcCount = switches.bmc.length; | |
| final int dataCount = switches.data.length; | |
| final int count = bmcCount + dataCount; | |
| final EtcDhcpHostEntry[] result = new EtcDhcpHostEntry[count]; | |
| final List<Inet4Address> bmcIPs = eh.bmcBlock.getHosts(); | |
| final int bmcIndex = bmcIPs.size() - 1 - EtcHosts.reserveCount; | |
| final List<Inet4Address> dataIPs = eh.dataBlock.getHosts(); | |
| final int dataIndex = dataIPs.size() - 1 - EtcHosts.reserveCount; | |
| for (int n = 0; n < bmcCount; ++n) { | |
| final ConfigData.SwitchData sd = switches.bmc[n]; | |
| final Inet4Address a = bmcIPs.get(bmcIndex - n); | |
| final String name = sd.name == null? sd.toToken(): sd.name; | |
| result[n] = new EtcDhcpHostEntry(name, a, sd.ethernet); | |
| } | |
| for (int n = 0; n < dataCount; ++n) { | |
| final ConfigData.SwitchData sd = switches.data[n]; | |
| final Inet4Address a = dataIPs.get(dataIndex - n); | |
| final String name = sd.name == null? sd.toToken(): sd.name; | |
| result[bmcCount + n] = new EtcDhcpHostEntry(name, a, sd.ethernet); | |
| } | |
| maybeThrowNotEnoughAddresses(eh, bmcCount, dataCount); | |
| return result; | |
| } | |
| /** | |
| * Return worker BMC entries using addresses from eh. | |
| */ | |
| private static EtcDhcpHostEntry[] addBmcs(EtcHosts eh) | |
| throws ApiException, IOException | |
| { | |
| final int count = eh.bmcWorkers.size(); | |
| final EtcDhcpHostEntry[] result = new EtcDhcpHostEntry[count]; | |
| for (int n = 0; n < count; ++n) { | |
| final EtcHostsEntry ehe = eh.bmcWorkers.get(n); | |
| result[n] = new EtcDhcpHostEntry( | |
| ehe.official, ehe.address, ehe.ethernet, "-bmc"); | |
| } | |
| return result; | |
| } | |
| /** | |
| * Return worker entries using addresses from eh using bootUrlIP. | |
| * A boot URL has the format | |
| * | |
| * http://172.20.4.254:9000/b...s/2012-05-15/m...t/04:7D:7B:62:91:54 | |
| * | |
| * where first is the manager IP address, second is the manager | |
| * API version, and third is the host's MAC address. | |
| */ | |
| private static EtcDhcpHostEntry[] addWorkers( | |
| EtcHosts eh, boolean noVlansHack) | |
| throws ApiException, IOException | |
| { | |
| final String urlIP = eh.getManagerIp().getHostAddress(); | |
| final String bootUrlFormat = | |
| "http://" + urlIP + ":9000/bootscripts/2012-05-15/mountroot/%s"; | |
| final int count = eh.dataWorkers.size(); | |
| final EtcDhcpHostEntry[] result = new EtcDhcpHostEntry[count]; | |
| for (int n = 0; n < count; ++n) { | |
| final EtcHostsEntry ehe = eh.dataWorkers.get(n); | |
| final String bootUrl = String.format(bootUrlFormat, ehe.ethernet); | |
| result[n] = new EtcDhcpHostEntry( | |
| ehe.official, ehe.address, ehe.ethernet, bootUrl); | |
| } | |
| return result; | |
| } | |
| /** | |
| * Write the two worker subnet clauses on bw. | |
| */ | |
| private final void writeWorkerSubnets(BufferedWriter bw) | |
| throws IOException | |
| { | |
| final String openFormat = | |
| "\nsubnet %s netmask %s {\n" + | |
| " next-server %s;\n"; | |
| final Inet4CidrBlock bmc = etcHosts.bmcBlock; | |
| final Inet4CidrBlock management = etcHosts.dataBlock; | |
| final String bmcOpen = | |
| String.format( | |
| openFormat, | |
| bmc.network.getHostAddress(), | |
| bmc.netmask.getHostAddress(), | |
| etcHosts.getManagerBmcIp().getHostAddress()); | |
| bw.write(bmcOpen); | |
| for (EtcDhcpHostEntry e : bmcs) bw.write(e.toString()); | |
| bw.write("}\n"); | |
| final String managementOpen = | |
| String.format( | |
| openFormat, | |
| management.network.getHostAddress(), | |
| management.netmask.getHostAddress(), | |
| etcHosts.getManagerIp().getHostAddress()); | |
| bw.write(managementOpen); | |
| for (EtcDhcpHostEntry e : workers) bw.write(e.toString()); | |
| bw.write("}\n"); | |
| } | |
| /** | |
| * Write the worker clauses out in the ip-mapping.js case. | |
| */ | |
| private final void writeWorkersWithNoVlansHack(BufferedWriter bw) | |
| throws IOException | |
| { | |
| final String openFormat = | |
| "subnet %s netmask %s {\n" + | |
| " option routers %s;\n" + | |
| " next-server %s;\n"; | |
| final ConfigData.NetworkConfiguration nc = | |
| configStore.networkConfiguration; | |
| final String open = | |
| String.format( | |
| openFormat, nc.subnet, nc.netmask, nc.router, | |
| Inet4CidrBlock.getRoutableLocalIp().getHostAddress()); | |
| bw.write(open); | |
| for (EtcDhcpHostEntry e : bmcs) bw.write(e.toString()); | |
| for (EtcDhcpHostEntry e : workers) bw.write(e.toString()); | |
| bw.write("}\n"); | |
| } | |
| /** | |
| * Write the content of this to temporary. | |
| */ | |
| private final MessageDigest writeTo(File temporary) | |
| throws ApiException, IOException, NoSuchAlgorithmException | |
| { | |
| final MessageDigest result = MessageDigest.getInstance(digestAlgorithm); | |
| final FileOutputStream fos = new FileOutputStream(temporary); | |
| final DigestOutputStream dos = new DigestOutputStream(fos, result); | |
| final BufferedWriter bw = | |
| new BufferedWriter(new OutputStreamWriter(dos)); | |
| try { | |
| bw.write(globals()); | |
| if (noVlansHack) { | |
| writeWorkersWithNoVlansHack(bw); | |
| } else { | |
| for (EtcDhcpHostEntry e : switches) bw.write(e.toString()); | |
| writeWorkerSubnets(bw); | |
| } | |
| } finally { | |
| bw.close(); | |
| dos.close(); | |
| fos.close(); | |
| } | |
| return result; | |
| } | |
| /** | |
| * Take a slight amount of care to write a .new file and return | |
| * true if the .new file's content differs from the original. | |
| * Otherwise return false. | |
| */ | |
| final boolean prepareChange() | |
| { | |
| boolean contentIsChanged = true; | |
| try { | |
| final File path = new File(pathName); | |
| final File directory = path.getParentFile(); | |
| final String leafName = path.getName(); | |
| final File temporary = new File(directory, leafName + ".new"); | |
| directory.mkdirs(); | |
| temporary.delete(); | |
| final MessageDigest newDigest = writeTo(temporary); | |
| final MessageDigest oldDigest = EtcHosts.digestFile(path); | |
| final byte[] newDb = newDigest.digest(); | |
| final byte[] oldDb = oldDigest.digest(); | |
| contentIsChanged = ! MessageDigest.isEqual(oldDb, newDb); | |
| } catch (Exception x) { | |
| throw new AssertionError(x); | |
| } | |
| return contentIsChanged; | |
| } | |
| /** | |
| * Run the command line in cmd and report its status. | |
| */ | |
| private static final void runCommand(String... cmd) | |
| { | |
| final ProcessWrapper pw = new ProcessWrapper(cmd); | |
| final StringBuilder sb = new StringBuilder(); | |
| for (String s : cmd) sb.append(" ").append(s); | |
| final String cmdLine = sb.toString(); | |
| try { | |
| final int status = pw.run(); | |
| final StringBuilder out = new StringBuilder(); | |
| try { | |
| out.append("stdout:").append(pw.getStdOut().trim()); | |
| } catch (IOException x) { | |
| logger.warning( | |
| String.format("%s: stdout threw: %s", cmdLine, x)); | |
| } | |
| try { | |
| out.append(", stderr:").append(pw.getStdErr().trim()); | |
| } catch (IOException x) { | |
| logger.warning( | |
| String.format("%s: stderr threw: %s", cmdLine, x)); | |
| } | |
| logger.info(String.format( | |
| "Command:%s ran with status %d and %s", | |
| cmdLine, status, out.toString())); | |
| } catch (Exception x) { | |
| logger.warning(String.format("%s: threw: %s", cmdLine, x)); | |
| } | |
| } | |
| /** | |
| * Swap the current file to .old and move .new in its place and | |
| * restart dhcpd. | |
| */ | |
| final void commitChange() | |
| { | |
| final File path = new File(pathName); | |
| final File directory = path.getParentFile(); | |
| final String leafName = path.getName(); | |
| final File temporary = new File(directory, leafName + ".new"); | |
| final String butNoVlans = noVlansHack? " but NO VLANS": ""; | |
| path.renameTo(new File(directory, leafName + ".old")); | |
| temporary.renameTo(path); | |
| logger.info("Wrote " + pathName + butNoVlans); | |
| runCommand(serviceName, "dhcpd", "restart"); | |
| } | |
| /** | |
| * Construct an /etc/dhcp/dhcp.conf file from networkInformation, | |
| * configStore, and what's cached in etcHosts. | |
| */ | |
| EtcDhcpDhcpdConf( | |
| EtcHosts etcHosts, | |
| NetworkInformation networkInformation, | |
| ConfigStore configStore) | |
| throws ApiException | |
| { | |
| this.noVlansHack = null == configStore.networkConfiguration.vlanId; | |
| this.configStore = configStore; | |
| this.etcHosts = etcHosts; | |
| this.domainName = networkInformation.workerIPConfig.domain; | |
| this.domainNameServers = networkInformation.workerIPConfig | |
| .dnsServers.toArray(new String[0]); | |
| try { | |
| this.switches = addSwitches(etcHosts, configStore.switches); | |
| this.bmcs = addBmcs(etcHosts); | |
| this.workers = addWorkers(etcHosts, noVlansHack); | |
| } catch (IOException x) { | |
| throw new AssertionError(x); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment