/*
* Copyright 1999-2008 University of Chicago
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.globus.workspace.network.defaults;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.globus.workspace.ProgrammingError;
import org.globus.workspace.Lager;
import org.globus.workspace.network.AssociationEntry;
import org.globus.workspace.network.Association;
import org.globus.workspace.persistence.PersistenceAdapter;
import org.globus.workspace.persistence.WorkspaceDatabaseException;
import org.nimbustools.api.services.rm.ResourceRequestDeniedException;
import org.nimbustools.api.services.rm.ManageException;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Enumeration;
public class Util {
private static final Log logger =
LogFactory.getLog(Util.class.getName());
private static final String NOENTRY = "none";
private static final String COMMENT_CHAR = "#";
/**
* @param name network name
* @param db db
* @param vmid for logging
* @param eventLog for logging
* @return Object[] length 2
* AssociationEntry [0]
* String dns setting [1]
* @throws ResourceRequestDeniedException denial
*/
static Object[] getNextEntry(String name,
PersistenceAdapter db,
int vmid,
boolean eventLog)
throws ResourceRequestDeniedException {
if (db == null) {
throw new IllegalArgumentException("null persistence adapter");
}
final Hashtable associations;
try {
associations = db.currentAssociations();
} catch (WorkspaceDatabaseException e) {
logger.fatal(e.getMessage(), e);
throw new ResourceRequestDeniedException(
"internal error, db problem");
}
final Object[] entryAndDns = nextAssociationEntry(name,
associations);
final AssociationEntry entry;
if (entryAndDns == null || entryAndDns[0] == null) {
final String err = "network '" + name
+ "' is not currently available";
logger.error(err);
throw new ResourceRequestDeniedException(err);
} else {
entry = (AssociationEntry) entryAndDns[0];
logger.debug("entry picked = " + entry);
}
try {
db.replaceAssociationEntry(name, entry);
} catch (WorkspaceDatabaseException e) {
logger.fatal(e.getMessage(), e);
throw new ResourceRequestDeniedException(
"internal error, db problem");
}
if (eventLog) {
logger.info(Lager.ev(vmid) + "'" + name + "' network " +
"entry leased, ip=" + entry.getIpAddress());
}
return entryAndDns;
}
static void retireEntry(String name,
String ipAddress,
PersistenceAdapter db,
int trackingID)
throws ManageException {
if (db == null) {
throw new IllegalArgumentException("null persistence adapter");
}
final Hashtable associations = db.currentAssociations();
final Association assoc = (Association)associations.get(name);
if (assoc == null) {
logger.error("no network '" + name + "'");
return;
}
final List entries = assoc.getEntries();
if (entries == null || entries.isEmpty()) {
logger.error(Lager.id(trackingID) +
" network '" + name + "' has no entries");
return;
}
// evidence for storing the entry list as an IP keyed hashtable...
final Iterator iter = entries.iterator();
AssociationEntry entry = null;
boolean found = false;
while (!found && iter.hasNext()) {
entry = (AssociationEntry)iter.next();
if (entry.getIpAddress().equals(ipAddress.trim())) {
found = true;
}
}
if (!found) {
throw new ManageException(Lager.id(trackingID) + " entry was " +
"not found in '" + name + "': " + ipAddress);
}
entry.setInUse(false);
db.replaceAssociationEntry(name, entry);
logger.info(Lager.ev(trackingID) + "'" + name + "' network lease " +
"is over, ip=" + entry.getIpAddress());
}
private static Object[] nextAssociationEntry(String name,
Hashtable associations)
throws ResourceRequestDeniedException {
final Association assoc = (Association)associations.get(name);
if (assoc == null) {
final String err = "'" + name + "' is not a valid network name";
logger.error(err);
throw new ResourceRequestDeniedException(err);
}
final List entries = assoc.getEntries();
if (entries == null || entries.isEmpty()) {
return null; // *** EARLY RETURN ***
}
final Iterator iter = assoc.getEntries().iterator();
AssociationEntry entry = null;
while (iter.hasNext()) {
entry = (AssociationEntry)iter.next();
if (!entry.isInUse()) {
entry.setInUse(true);
break;
}
entry = null;
}
if (entry == null) {
return null; // *** EARLY RETURN ***
}
final Object[] objs = new Object[2];
objs[0] = entry;
final String DNS = assoc.getDns();
if (DNS == null || DNS.equalsIgnoreCase("none")) {
objs[1] = "null";
} else {
objs[1] = DNS;
}
return objs;
}
/**
* @param associationDir association directory, may not be null
* @param previous previous entries
* @return updated entries
* @throws Exception problem
*/
static Hashtable loadDirectory(File associationDir,
Hashtable previous) throws Exception {
if (associationDir == null) {
throw new IllegalArgumentException("null associationDir");
}
final String[] listing = associationDir.list();
if (listing == null) {
// null return from list() is different than zero results, it denotes a real problem
throw new Exception("Problem listing contents of directory '" +
associationDir.getAbsolutePath() + '\'');
}
final Hashtable newAssocSet = new Hashtable(listing.length);
for (int i = 0; i < listing.length; i++) {
final String path = associationDir.getAbsolutePath()
+ File.separator + listing[i];
final File associationFile = new File(path);
if (!associationFile.isFile()) {
logger.warn("not a file: '" + path + "'");
continue;
}
final String assocName = associationFile.getName();
Association oldassoc = null;
if (previous != null) {
oldassoc = (Association) previous.get(assocName);
// skip reading if file modification time isn't newer than last
// container boot
if (oldassoc != null) {
if (oldassoc.getFileTime() ==
associationFile.lastModified()) {
logger.info("file modification time for network '"
+ assocName
+ "' is not newer, using old configuration");
newAssocSet.put(assocName,
oldassoc);
continue;
}
}
}
final Association newassoc =
getNewAssoc(assocName, associationFile, oldassoc);
if (newassoc != null) {
newAssocSet.put(assocName, newassoc);
}
}
if (previous == null || previous.isEmpty()) {
return newAssocSet;
}
// Now look at previous entries in database for entries that were
// there and now entirely gone.
// If in use, we don't do anything. When retired and the entry is
// not in DB, a warning will trip but that is it. From then on, the
// address will be gone.
final Enumeration en = previous.keys();
while (en.hasMoreElements()) {
final String assocname = (String) en.nextElement();
final Association oldassoc = (Association) previous.get(assocname);
if (oldassoc == null) {
throw new ProgrammingError("all networks " +
"in the hashmap should be non-null");
}
if (newAssocSet.containsKey(assocname)) {
logChangedAssoc(assocname,
(Association)newAssocSet.get(assocname),
oldassoc);
} else {
logger.info("Previously configured network '" + assocname +
"' is not present in the new configuration. " +
goneStatus(oldassoc));
}
}
return newAssocSet;
}
// Parses a new association, if one with same name existed before,
// this compares the two.
// If an entry existed with the same IP address, the entry is reconfigured
// entirely from the new file's information. If the entry is currently
// in use this is recorded.
private static Association getNewAssoc(String assocName,
File file,
Association oldassoc)
throws IOException {
final Association assoc = loadOne(file);
if (assoc == null) {
return null;
}
if (oldassoc == null) {
return assoc;
}
final List assocEntries = assoc.getEntries();
if (assocEntries == null || assocEntries.isEmpty()) {
// no conflicts are possible
return assoc;
}
final List oldassocEntries = oldassoc.getEntries();
if (oldassocEntries == null || oldassocEntries.isEmpty()) {
return assoc;
}
if (assoc.getDns() != null && !assoc.getDns().equals(oldassoc.getDns())) {
logger.info("Network '" + assocName + "': DNS changed from " +
oldassoc.getDns() + " to " + assoc.getDns());
}
for (Object assocEntry : assocEntries) {
final AssociationEntry entry = (AssociationEntry) assocEntry;
final AssociationEntry oldentry =
getMatchingIpEntry(entry.getIpAddress(),
oldassocEntries);
if (oldentry == null) {
continue;
}
logDifferences(assocName, entry, oldentry);
// Any change is OK.
// We know it has same IP address and that is enough to retire
// with. But the in-use flag MUST match the old one.
entry.setInUse(oldentry.isInUse());
if (!entry.isExplicitMac()) {
entry.setMac(oldentry.getMac());
}
if (entry.isInUse()) {
logger.debug("Network '" + assocName + "', ip " +
entry.getIpAddress() + " is currently in use.");
}
}
return assoc;
}
private static AssociationEntry getMatchingIpEntry(String ip,
List entries) {
if (ip == null) {
throw new IllegalArgumentException("ip is null");
}
final Iterator iter = entries.iterator();
while (iter.hasNext()) {
final AssociationEntry entry = (AssociationEntry) iter.next();
if (ip.equals(entry.getIpAddress())) {
return entry;
}
}
return null;
}
private static void logDifferences(String assocName,
AssociationEntry entry,
AssociationEntry oldentry) {
boolean same = true;
final StringBuffer buf = new StringBuffer("Network '");
buf.append(assocName)
.append("', ip ")
.append(entry.getIpAddress())
.append(": has differences in new configuration. ");
if (diffErator(buf,
"hostname",
entry.getHostname(),
oldentry.getHostname())) {
same = false;
}
if (diffErator(buf,
"gateway",
entry.getGateway(),
oldentry.getGateway())) {
same = false;
buf.append("; ");
}
if (diffErator(buf,
"netmask",
entry.getSubnetMask(),
oldentry.getSubnetMask())) {
same = false;
buf.append("; ");
}
if (diffErator(buf,
"broadcast",
entry.getBroadcast(),
oldentry.getBroadcast())) {
same = false;
buf.append("; ");
}
if (entry.isExplicitMac() &&
diffErator(buf, "MAC",
entry.getMac(),
oldentry.getMac())) {
same = false;
buf.append("; ");
}
if (!same) {
logger.info(buf.toString());
}
}
// adds string explanation of any difference, returns true if different
private static boolean diffErator(StringBuffer buf,
String fieldname,
String newval,
String oldval) {
if (oldval == null && newval == null) {
return false;
}
if (oldval != null && newval != null) {
if (oldval.equals(newval)) {
return false;
}
}
String oldstr = "none";
String newstr = "none";
if (oldval != null) {
oldstr = "'" + oldval + "'";
}
if (newval != null) {
newstr = "'" + newval + "'";
}
buf.append(fieldname)
.append(" ")
.append(oldstr)
.append("-->")
.append(newstr)
.append("; ");
return true;
}
private static Association loadOne(File file)
throws IOException {
Association association = null;
final List associationList = new LinkedList();
// Read the contents of file
InputStream in = null;
InputStreamReader isr = null;
BufferedReader bufrd = null;
String line;
try {
in = new FileInputStream(file);
isr = new InputStreamReader(in);
bufrd = new BufferedReader(isr);
line = bufrd.readLine();
if (line != null) {
// find first non-comment, non-empty line
boolean notfound = true;
while (notfound) {
try {
association = parseDNS(line);
notfound = false;
} catch (Exception e) {
line = bufrd.readLine();
if (line == null) {
association = null;
break;
}
}
}
if (association == null) {
logger.error("DNS information incorrectly" +
" specified, skipping entire network. " +
"Path: " + file.getAbsolutePath());
return null;
}
} else {
logger.warn("network file '" + file.getAbsolutePath() +
"' is empty, skipping");
return null;
}
// the rest
while ((line = bufrd.readLine()) != null) {
line = line.trim();
if (line.length() > 0) {
AssociationEntry entry = parseAssoc(line);
if (entry != null) {
associationList.add(entry);
}
}
// can have an association with no entries
}
} finally {
try {
if (bufrd != null) {
bufrd.close();
}
if (isr != null) {
isr.close();
}
if (in != null) {
in.close();
}
} catch (Exception e) {
logger.error("",e);
}
}
association.setEntries(associationList);
association.setFileTime(file.lastModified());
return association;
}
private static Association parseDNS(String line)
throws CommentException, BlankLineException {
if (line == null) {
return null;
}
final StringTokenizer st = new StringTokenizer(line);
if (st.countTokens() == 0) {
throw new BlankLineException();
}
final String dns = st.nextToken().trim();
if (dns.startsWith(COMMENT_CHAR)) {
throw new CommentException();
}
if (dns.equals(NOENTRY)) {
return new Association(null);
} else {
return new Association(dns);
}
}
private static AssociationEntry parseAssoc(String line) {
if (line == null) {
return null;
}
final StringTokenizer st = new StringTokenizer(line);
// don't note blank, cosmetic lines
if (st.countTokens() == 0) {
return null;
}
// ignore comments
final String hostname = st.nextToken().trim();
if (hostname.startsWith(COMMENT_CHAR)) {
return null;
}
final int tokens = st.countTokens();
if (tokens != 4 && tokens != 5) {
logger.error("entry in network file is invalid. Expecting either" +
" five or six tokens (MAC optional)" +
" -- line = '" + line + "'");
return null;
}
if (hostname.equals(NOENTRY)) {
logger.error("network entry must contain hostname" +
" in first position -- line = '" + line + "'");
return null;
}
final String ipaddress = st.nextToken().trim();
if (ipaddress.equals(NOENTRY)) {
logger.error("network entry must contain IP" +
" address in second position -- line = '" + line + "'");
return null;
}
String gateway = st.nextToken().trim();
if (gateway.equals(NOENTRY)) {
// perhaps they do not need other network access
gateway = null;
}
String broadcast = st.nextToken().trim();
if (broadcast.equals(NOENTRY)) {
broadcast = null;
}
String subnetmask = st.nextToken().trim();
if (subnetmask.equals(NOENTRY)) {
subnetmask = null;
}
// mac can be optionally supplied as final token
String mac = null;
if (tokens == 5) {
mac = st.nextToken().trim();
if (mac.equals(NOENTRY)) {
mac = null;
} else {
if (!MacUtil.isValidMac(mac, false)) {
logger.error("Invalid MAC address entry -- line = '" +
line + "'.");
return null;
}
}
}
final AssociationEntry entry =
new AssociationEntry(ipaddress, mac, hostname,
gateway, broadcast, subnetmask);
entry.setExplicitMac(mac != null);
return entry;
}
private static void logChangedAssoc(String assocname,
Association newassoc,
Association oldassoc) {
final List oldentries = oldassoc.getEntries();
final List newentries = newassoc.getEntries();
final Iterator oldEntriesIter = oldentries.iterator();
while (oldEntriesIter.hasNext()) {
final AssociationEntry oldentry =
(AssociationEntry) oldEntriesIter.next();
boolean foundOldEntry = false;
final Iterator newEntriesIter = newentries.iterator();
while (newEntriesIter.hasNext()) {
final AssociationEntry newentry =
(AssociationEntry) newEntriesIter.next();
if (oldentry.getIpAddress().equals(newentry.getIpAddress())) {
foundOldEntry = true;
break;
}
}
if (!foundOldEntry) {
String inuse = "";
if (oldentry.isInUse()) {
inuse = " Note it is currently in use.";
}
logger.info("IP '" + oldentry.getIpAddress() +
"' is not present in the new " +
"configuration for network '" + assocname +
"'. Deleted from available " +
"addresses in address pool." + inuse);
}
}
}
// the entire old pool was removed, log what we can
private static String goneStatus(Association oldassoc) {
final List oldentries = oldassoc.getEntries();
if (oldentries == null || oldentries.isEmpty()) {
return "There were no addresses in that network.";
}
final StringBuffer buf = new StringBuffer("Contents: ");
final Iterator oldEntriesIter = oldentries.iterator();
while (oldEntriesIter.hasNext()) {
final AssociationEntry entry =
(AssociationEntry) oldEntriesIter.next();
buf.append("ip '")
.append(entry.getIpAddress())
.append("' host '")
.append(entry.getHostname())
.append("' inuse? ")
.append(entry.isInUse())
.append("; ");
}
return buf.toString();
}
private static class CommentException extends Exception {}
private static class BlankLineException extends Exception {}
}