Skip to content

Instantly share code, notes, and snippets.

@tbl3rd
Created April 4, 2015 14:13
Show Gist options
  • Save tbl3rd/bc38d1d7c1deb2ca66d1 to your computer and use it in GitHub Desktop.
Save tbl3rd/bc38d1d7c1deb2ca66d1 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.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Inet4Address;
import java.security.DigestInputStream;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Logger;
/**
* A line from an /etc/hosts file specifying an official name for the
* IP address along with its aliases.
*/
final class EtcHostsEntry {
static final String addressField = "256.256.256.256 ";
static final String entryFormat = "%-" + addressField.length() + "s%s";
final Inet4Address address;
final String ethernet;
final String official;
final String[] aliases;
@Override
public final String toString()
{
final StringBuilder result = new StringBuilder();
final Formatter f = new Formatter(result);
f.format(entryFormat, address.getHostAddress(), official);
for (String a : aliases) result.append(" ").append(a);
return result.toString();
}
EtcHostsEntry(
Inet4Address address,
String ethernet,
String official,
String... aliases)
{
this.address = address;
this.ethernet = ethernet;
this.official = official;
this.aliases = aliases;
}
}
/**
* The contents of an /etc/hosts file constructed from
* NetworkInformation.
*/
final class EtcHosts
{
private final static Logger logger =
Logger.getLogger("tbl3rd.provider");
/**
* The number of IP addresses reserved from each of the BMC and
* data subnets for the manager node itself (and backup).
*/
static final int reserveCount =
Integer.getInteger(EtcHosts.class.getName() + ".reserveCount", 5);
/**
* Manager names on the data network.
*/
private static final String managementNode =
System.getProperty(
EtcHosts.class.getName() + ".managementNode",
"management-node");
private static final String managementNode1 =
System.getProperty(
EtcHosts.class.getName() + ".managementNode1",
"management-node-1");
private static final String managementNode2 =
System.getProperty(
EtcHosts.class.getName() + ".managementNode1",
"management-node-2");
/*
* Names for management node interfaces on each BMC IP subnet.
*/
private static final String managementNode1BmcnetInterface =
System.getProperty(
EtcHosts.class.getName() + ".managementNode1BmcnetInterface",
"management-node-1-bmcnet-interface");
private static final String managementNode2BmcnetInterface =
System.getProperty(
EtcHosts.class.getName() + ".managementNode2BmcnetInterface",
"management-node-2-bmcnet-interface");
/*
* Names for the management node BMCs (which just happen to be on
* the BMC IP subnet).
*/
private static final String managementNode1Bmc =
System.getProperty(
EtcHosts.class.getName() + ".managementNode1Bmc",
"management-node-1-bmc");
private static final String managementNode2Bmc =
System.getProperty(
EtcHosts.class.getName() + ".managementNode2Bmc",
"management-node-2-bmc");
private static final String pathName =
System.getProperty(
EtcHosts.class.getName() + ".pathName", "/etc/hosts");
private static final String digestAlgorithm =
System.getProperty(
EtcHosts.class.getName() + ".digestAlgorithm", "SHA-1");
private static final String separatorLine =
System.getProperty(
EtcHosts.class.getName() + ".separatorLine",
"# THIS LINE AND REST OF FILE MACHINE GENERATED : " +
"DO NOT EDIT BY HAND");
private static final String defaultTop = System.getProperty(
EtcHosts.class.getName() + ".defaultTop",
"127.0.0.1 localhost localhost.localdomain " +
"localhost4 localhost4.localdomain4" + "\n" +
"::1 localhost localhost.localdomain " +
"localhost6 localhost6.localdomain6" + "\n");
/**
* Entries that depend on configuration.
*/
final ArrayList<EtcHostsEntry> dataManagers;
final ArrayList<EtcHostsEntry> bmcManagers;
final ArrayList<EtcHostsEntry> dataWorkers;
final ArrayList<EtcHostsEntry> bmcWorkers;
final ArrayList<EtcHostsEntry> bmcSwitches;
final ArrayList<EtcHostsEntry> dataSwitches;
private final ArrayList<EtcHostsEntry> publicWorkers;
private final boolean noVlansHack;
/**
* Bookkeeping set up by constructor.
*/
final Inet4CidrBlock dataBlock;
final Inet4CidrBlock bmcBlock;
private final List<Inet4Address> publicAddresses;
/**
* Return the data IP address for the management-node.
*/
final Inet4Address getManagerIp()
{
return dataManagers.get(0).address;
}
/**
* Return the BMC IP address for the management-node.
*/
final Inet4Address getManagerBmcIp()
{
return bmcManagers.get(0).address;
}
/**
* Add switch entries for a VLAN configuration allocating
* addresses from the data block m and the BMC block b.
*/
private final void addVlanSwitchEntries(
ConfigStore cs, List<Inet4Address> m, List<Inet4Address> b)
{
for (int n = 0; n < cs.switches.bmc.length; ++n) {
final ConfigData.SwitchData sd = cs.switches.bmc[n];
final String name = sd.name == null? sd.toToken(): sd.name;
final int i = b.size() - reserveCount - n - 1;
bmcSwitches.add(n, new EtcHostsEntry(b.get(i), sd.ethernet, name));
}
for (int n = 0; n < cs.switches.data.length; ++n) {
final ConfigData.SwitchData sd = cs.switches.data[n];
final String name = sd.name == null? sd.toToken(): sd.name;
final int i = m.size() - reserveCount - n - 1;
dataSwitches.add(n, new EtcHostsEntry(m.get(i), sd.ethernet, name));
}
}
/**
* Add manager entries for a VLAN configuration allocating
* addresses from the data block m and the BMC block b.
*
* The reserved-data-N names align addresses between the
* bmc and data subnets.
*
* reserved-bmc-1 reserves a BMC address corresponding to the
* floating data address for the management-node.
*/
private final void addVlanManagerEntries(
List<Inet4Address> m, List<Inet4Address> b)
{
dataManagers.add(0, new EtcHostsEntry(
m.get(m.size() - 1), null, managementNode));
dataManagers.add(1, new EtcHostsEntry(
m.get(m.size() - 2), null, managementNode1));
dataManagers.add(2, new EtcHostsEntry(
m.get(m.size() - 3), null, managementNode2));
dataManagers.add(3, new EtcHostsEntry(
m.get(m.size() - 4), null, "reserved-data-4"));
dataManagers.add(4, new EtcHostsEntry(
m.get(m.size() - 5), null, "reserved-data-5"));
bmcManagers.add(0, new EtcHostsEntry(b.get(m.size() - 1), null,
"reserved-bmc-1"));
bmcManagers.add(1, new EtcHostsEntry(b.get(m.size() - 2), null,
managementNode1BmcnetInterface));
bmcManagers.add(2, new EtcHostsEntry(b.get(m.size() - 3), null,
managementNode2BmcnetInterface));
bmcManagers.add(3, new EtcHostsEntry(b.get(m.size() - 4), null,
managementNode1Bmc));
bmcManagers.add(4, new EtcHostsEntry(b.get(m.size() - 5), null,
managementNode2Bmc));
}
/**
* Add entries configured for VLANs using cs.
*/
private final void addVlanEntries(ConfigStore cs)
throws ApiException, IOException
{
final ConfigData.WorkerData[] worker =
cs.workerTable.values().toArray(new ConfigData.WorkerData[0]);
Arrays.sort(worker);
final List<Inet4Address> m = this.dataBlock.getHosts();
final List<Inet4Address> b = this.bmcBlock.getHosts();
final List<Inet4Address> p = this.publicAddresses;
final int count = cs.workerTable.size();
for (int n = 0; n < count; ++n) {
final ConfigData.WorkerData w = worker[n];
final String t = w.toToken();
final String mE = w.MACAddresses.gbe0;
final String bE = w.MACAddresses.bmc;
dataWorkers.add(
new EtcHostsEntry(m.get(n), mE, t));
bmcWorkers.add(
new EtcHostsEntry(b.get(n), bE, t + "-bmc"));
publicWorkers.add(
new EtcHostsEntry(p.get(n), mE, t + "-public"));
}
addVlanManagerEntries(m, b);
addVlanSwitchEntries(cs, m, b);
}
/**
* Add entries configured without VLANs using cs.
*/
private final void addNoVlanEntriesHack(ConfigStore cs)
throws ApiException, IOException
{
final ConfigData.WorkerData[] worker =
cs.workerTable.values().toArray(new ConfigData.WorkerData[0]);
Arrays.sort(worker);
final int count = worker.length;
for (int n = 0; n < count; ++n) {
final ConfigData.WorkerData wd = worker[n];
final String t = wd.toToken();
final ConfigData.IpMapping ipm = cs.ipMapping.get(wd);
if (ipm == null) throw new AssertionError("No IP for " + t);
final String mE = wd.MACAddresses.gbe0;
final String bE = wd.MACAddresses.bmc;
final Inet4Address mI = Inet4CidrBlock.getByName(ipm.main);
final Inet4Address bI = Inet4CidrBlock.getByName(ipm.bmc);
final EtcHostsEntry m = Inet4CidrBlock.isIp(ipm.main)?
new EtcHostsEntry(mI, mE, t, t + "-public"):
new EtcHostsEntry(mI, mE, t, t + "-public", ipm.main);
dataWorkers.add(m);
final EtcHostsEntry b = Inet4CidrBlock.isIp(ipm.bmc)?
new EtcHostsEntry(bI, bE, t + "-bmc"):
new EtcHostsEntry(bI, bE, t + "-bmc", ipm.bmc);
bmcWorkers.add(b);
}
final Inet4Address a = Inet4CidrBlock.getRoutableLocalIp();
dataManagers.add(0, new EtcHostsEntry(a, null, managementNode));
}
/**
* Return the top lines of /etc/hosts up to the separatorLine,
* with the separatorLine appended.
*/
private static final String readTopOfFile()
throws IOException
{
final StringBuilder result = new StringBuilder();
try {
final FileInputStream fis = new FileInputStream(pathName);
final BufferedReader br =
new BufferedReader(new InputStreamReader(fis));
try {
while (true) {
final String line = br.readLine();
if (line == null) break;
if (separatorLine.equals(line)) break;
result.append(line).append("\n");
}
} finally {
br.close();
fis.close();
}
} catch (IOException x) {
logger.info(
String.format("Cannot open %s file: %s", pathName, x));
}
if (result.length() == 0) result.append(defaultTop);
result.append(separatorLine).append("\n");
return result.toString();
}
/**
* Throw an exception if e.address is already in all.
*/
private static final void throwIfDuplicateEntry(
HashMap<Inet4Address, EtcHostsEntry> all,
EtcHostsEntry e)
throws ApiException
{
if (all.containsKey(e.address)) {
final String message =
String.format(
"%s file with duplicate address: was \"%s\" is \"%s\"",
pathName, all.get(e.address), e);
throw new ApiException(
ManagementApi_2012_05_15.ResultCode.BAD_REQUEST,
message);
}
all.put(e.address, e);
}
/**
* Write e on bw or throw if e's address is in all.
*/
private static void writeEntry(
HashMap<Inet4Address, EtcHostsEntry> all,
BufferedWriter bw,
EtcHostsEntry e)
throws ApiException, IOException
{
throwIfDuplicateEntry(all, e);
bw.write(e.toString()); bw.write("\n");
}
/**
* Write the content of this to temporary, preserving the top
* lines of /etc/hosts before the separatorLine and return a
* digest of the content.
*/
private final MessageDigest writeTo(File temporary)
throws ApiException, IOException, NoSuchAlgorithmException
{
final MessageDigest result = MessageDigest.getInstance(digestAlgorithm);
final HashMap<Inet4Address, EtcHostsEntry> all =
new HashMap<Inet4Address, EtcHostsEntry>();
final FileOutputStream fos = new FileOutputStream(temporary);
final DigestOutputStream dos = new DigestOutputStream(fos, result);
final BufferedWriter bw =
new BufferedWriter(new OutputStreamWriter(dos));
try {
bw.write(readTopOfFile());
for (EtcHostsEntry e : dataManagers) writeEntry(all, bw, e);
for (EtcHostsEntry e : dataSwitches) writeEntry(all, bw, e);
for (EtcHostsEntry e : bmcManagers) writeEntry(all, bw, e);
for (EtcHostsEntry e : bmcSwitches) writeEntry(all, bw, e);
for (EtcHostsEntry e : dataWorkers) writeEntry(all, bw, e);
for (EtcHostsEntry e : bmcWorkers) writeEntry(all, bw, e);
for (EtcHostsEntry e : publicWorkers) writeEntry(all, bw, e);
} finally {
bw.close();
dos.close();
fos.close();
}
return result;
}
/**
* Return a digest of f. Just warn if there is no f, because it
* will be created later.
*/
static MessageDigest digestFile(File f)
throws IOException, NoSuchAlgorithmException
{
final MessageDigest result = MessageDigest.getInstance("SHA-1");
try {
final FileInputStream fis = new FileInputStream(f);
final DigestInputStream dis = new DigestInputStream(fis, result);
final BufferedReader br =
new BufferedReader(new InputStreamReader(dis));
try {
char[] ignored = new char[4096];
int count = br.read(ignored);
while (count != -1) count = br.read(ignored);
} finally {
br.close();
dis.close();
fis.close();
}
} catch (IOException x) {
logger.info(
String.format("Cannot read %s file: %s", f.getPath(), x));
}
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;
}
/**
* Swap the current file to .old and move .new in its place.
*/
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);
}
/**
* Throw an exception if there is some kind of problem.
*/
private static final void maybeThrowNotEnoughAddresses(
ArrayList<String> kind, long[] required, long[] provided)
throws ApiException
{
final long problemCount = kind.size();
if (problemCount == 0) return;
final StringBuilder sb = new StringBuilder(
String.format(
"Not enough addresses for %s file.", pathName));
for (int n = 0; n < problemCount; ++n) {
final String problem =
String.format("Need %d %s addresses, but %d provided.",
required[n], kind.get(n), provided[n]);
sb.append("\n").append(problem);
}
throw new ApiException(
ManagementApi_2012_05_15.ResultCode.BAD_REQUEST,
sb.toString());
}
/**
* Throw if there are not enough addresses for workerCount
* workers.
*/
private final void workerCountOk(int workerCount)
throws ApiException
{
final long[] required = {0, 0, 0};
final long[] provided = {0, 0, 0};
final ArrayList<String> kind = new ArrayList<String>();
if (workerCount + reserveCount > dataBlock.hostCount()) {
required[kind.size()] = workerCount + reserveCount;
provided[kind.size()] = dataBlock.hostCount();
kind.add("management");
}
if (workerCount + reserveCount > bmcBlock.hostCount()) {
required[kind.size()] = workerCount + reserveCount;
provided[kind.size()] = bmcBlock.hostCount();
kind.add("bmc");
}
if (workerCount > publicAddresses.size()) {
required[kind.size()] = workerCount;
provided[kind.size()] = publicAddresses.size();
kind.add("public");
}
maybeThrowNotEnoughAddresses(kind, required, provided);
}
/**
* Construct the content of an /etc/hosts file from ni and cs.
*/
EtcHosts(NetworkInformation ni, ConfigStore cs)
throws ApiException
{
this.noVlansHack = null == cs.networkConfiguration.vlanId;
final int wCount =
noVlansHack? cs.ipMapping.size(): cs.workerTable.size();
try {
this.dataBlock = new Inet4CidrBlock(ni.managementIPSubnet);
this.bmcBlock = new Inet4CidrBlock(ni.bmcIPSubnet);
this.publicAddresses =
Inet4CidrBlock.parseAddressPool(ni.workerIPConfig.addressPool);
this.workerCountOk(cs.workerTable.size());
this.dataWorkers = new ArrayList<EtcHostsEntry>(wCount);
this.bmcWorkers = new ArrayList<EtcHostsEntry>(wCount);
this.publicWorkers = new ArrayList<EtcHostsEntry>(wCount);
this.dataManagers = new ArrayList<EtcHostsEntry>(reserveCount);
this.bmcManagers = new ArrayList<EtcHostsEntry>(reserveCount);
this.bmcSwitches =
new ArrayList<EtcHostsEntry>(cs.switches.bmc.length);
this.dataSwitches =
new ArrayList<EtcHostsEntry>(cs.switches.data.length);
if (this.noVlansHack) {
addNoVlanEntriesHack(cs);
} else {
addVlanEntries(cs);
}
} 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