Created
April 4, 2015 14:13
-
-
Save tbl3rd/bc38d1d7c1deb2ca66d1 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.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