Skip to content

Instantly share code, notes, and snippets.

@tbl3rd
Created April 4, 2015 14:14
Show Gist options
  • Select an option

  • Save tbl3rd/32d7fcd902a004f3e632 to your computer and use it in GitHub Desktop.

Select an option

Save tbl3rd/32d7fcd902a004f3e632 to your computer and use it in GitHub Desktop.
manage private LAN addresses
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