diff --git a/src/main/java/net/floodlightcontroller/core/IFloodlightProviderService.java b/src/main/java/net/floodlightcontroller/core/IFloodlightProviderService.java
index be61662c5e190993b8b0a63efd79faa4df1a6413..ba898d2d852d7d256e330f0592978498fec7527c 100644
--- a/src/main/java/net/floodlightcontroller/core/IFloodlightProviderService.java
+++ b/src/main/java/net/floodlightcontroller/core/IFloodlightProviderService.java
@@ -113,13 +113,13 @@ public interface IFloodlightProviderService extends IFloodlightService {
      * Adds a listener for HA role events
      * @param listener The module that wants to listen for events
      */
-    public void addHAListener(IHARoleListener listener);
+    public void addHAListener(IHAListener listener);
     
     /**
      * Removes a listener for HA role events
      * @param listener The module that no longer wants to listen for events
      */
-    public void removeHAListener(IHARoleListener listener);
+    public void removeHAListener(IHAListener listener);
 
     /**
      * Terminate the process
diff --git a/src/main/java/net/floodlightcontroller/core/IHAListener.java b/src/main/java/net/floodlightcontroller/core/IHAListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..c76f46a5fde13d5e28bfe9fdb9fe5dbd1507e1e0
--- /dev/null
+++ b/src/main/java/net/floodlightcontroller/core/IHAListener.java
@@ -0,0 +1,30 @@
+package net.floodlightcontroller.core;
+
+import java.util.Map;
+
+import net.floodlightcontroller.core.IFloodlightProviderService.Role;
+
+public interface IHAListener {
+    /**
+     * Gets called when the controller changes role (i.e. Master -> Slave).
+     * Note that oldRole CAN be null.
+     * @param oldRole The controller's old role
+     * @param newRole The controller's new role
+     */
+    public void roleChanged(Role oldRole, Role newRole);
+    
+    /**
+     * Gets called when the IP addresses of the controller nodes in the 
+     * controller cluster change. All parameters map controller ID to
+     * the controller's IP.
+     *  
+     * @param curControllerNodeIPs The current mapping of controller IDs to IP
+     * @param addedControllerNodeIPs These IPs were added since the last update
+     * @param removedControllerNodeIPs These IPs were removed since the last update
+     */
+    public void controllerNodeIPsChanged(
+    		Map<String, String> curControllerNodeIPs,  
+    		Map<String, String> addedControllerNodeIPs,  
+    		Map<String, String> removedControllerNodeIPs
+    		);
+}
diff --git a/src/main/java/net/floodlightcontroller/core/IHARoleListener.java b/src/main/java/net/floodlightcontroller/core/IHARoleListener.java
deleted file mode 100644
index 83749caed0d7e33fb2d177d4837d43b2d95f5d97..0000000000000000000000000000000000000000
--- a/src/main/java/net/floodlightcontroller/core/IHARoleListener.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.floodlightcontroller.core;
-
-import net.floodlightcontroller.core.IFloodlightProviderService.Role;
-
-public interface IHARoleListener {
-    /**
-     * Gets called when the controller changes role (i.e. Master -> Slave).
-     * Note that oldRole CAN be null.
-     * @param oldRole The controller's old role
-     * @param newRole The controller's new role
-     */
-    public void roleChanged(Role oldRole, Role newRole);
-}
diff --git a/src/main/java/net/floodlightcontroller/core/internal/Controller.java b/src/main/java/net/floodlightcontroller/core/internal/Controller.java
index c087bf217c052cbdafcbce314947c2683c7a2685..78db8ee1120a97752b4be48baceb889eb6ece15e 100644
--- a/src/main/java/net/floodlightcontroller/core/internal/Controller.java
+++ b/src/main/java/net/floodlightcontroller/core/internal/Controller.java
@@ -48,7 +48,7 @@ import java.util.concurrent.TimeoutException;
 
 import net.floodlightcontroller.core.FloodlightContext;
 import net.floodlightcontroller.core.IFloodlightProviderService;
-import net.floodlightcontroller.core.IHARoleListener;
+import net.floodlightcontroller.core.IHAListener;
 import net.floodlightcontroller.core.IInfoProvider;
 import net.floodlightcontroller.core.IOFMessageListener;
 import net.floodlightcontroller.core.IOFMessageListener.Command;
@@ -63,6 +63,7 @@ import net.floodlightcontroller.packet.Ethernet;
 import net.floodlightcontroller.perfmon.IPktInProcessingTimeService;
 import net.floodlightcontroller.restserver.IRestApiService;
 import net.floodlightcontroller.storage.IResultSet;
+import net.floodlightcontroller.storage.IStorageSourceListener;
 import net.floodlightcontroller.storage.IStorageSourceService;
 import net.floodlightcontroller.storage.OperatorPredicate;
 import net.floodlightcontroller.storage.StorageException;
@@ -129,7 +130,8 @@ import org.slf4j.LoggerFactory;
 /**
  * The main controller class.  Handles all setup and network listeners
  */
-public class Controller implements IFloodlightProviderService {
+public class Controller implements IFloodlightProviderService, 
+            IStorageSourceListener {
     
     protected static Logger log = LoggerFactory.getLogger(Controller.class);
     
@@ -146,10 +148,14 @@ public class Controller implements IFloodlightProviderService {
     // send role request messages to the switches when our role changes to master
     protected ConcurrentHashMap<Long, IOFSwitch> connectedSwitches;
     
+    // The controllerNodeIPsCache maps Controller IDs to their IP address. 
+    // It's only used by handleControllerNodeIPsChanged
+    protected HashMap<String, String> controllerNodeIPsCache;
+    
     protected Set<IOFSwitchListener> switchListeners;
-    protected Set<IHARoleListener> haListeners;
+    protected Set<IHAListener> haListeners;
     protected Map<String, List<IInfoProvider>> providerMap;
-    protected BlockingQueue<Update> updates;
+    protected BlockingQueue<IUpdate> updates;
     
     // Module dependencies
     protected IRestApiService restApi;
@@ -197,31 +203,107 @@ public class Controller implements IFloodlightProviderService {
     protected static final String PORT_SUPPORTED_FEATURES = "supported_features";
     protected static final String PORT_PEER_FEATURES = "peer_features";
     
+    protected static final String CONTROLLER_INTERFACE_TABLE_NAME = "controller_controllerinterface";
+    protected static final String CONTROLLER_INTERFACE_ID = "id";
+    protected static final String CONTROLLER_INTERFACE_CONTROLLER_ID = "controller_id";
+    protected static final String CONTROLLER_INTERFACE_TYPE = "type";
+    protected static final String CONTROLLER_INTERFACE_NUMBER = "number";
+    protected static final String CONTROLLER_INTERFACE_DISCOVERED_IP = "discovered_ip";
+    
+    
+    
     // Perf. related configuration
     protected static final int SEND_BUFFER_SIZE = 4 * 1024 * 1024;
     protected static final int BATCH_MAX_SIZE = 100;
     protected static final boolean ALWAYS_DECODE_ETH = true;
 
-    protected enum UpdateType {
-        SWITCH, HA
+    /**
+     *  Updates handled by the main loop 
+     */
+    protected interface IUpdate {
+        /** 
+         * Calls the appropriate listeners
+         */
+        public void dispatch();
     }
-    protected class Update {
-        public UpdateType type;
+    /**
+     * A switch was added or removed 
+     */
+    protected class SwitchUpdate implements IUpdate {
         public IOFSwitch sw;
         public boolean added;
-        public Role oldRole;
-        public Role newRole;
-
-        public Update(IOFSwitch sw, boolean added) {
+        public SwitchUpdate(IOFSwitch sw, boolean added) {
             this.sw = sw;
             this.added = added;
-            this.type = UpdateType.SWITCH;
         }
-        
-        public Update(Role newRole, Role oldRole) {
+        public void dispatch() {
+            if (log.isDebugEnabled()) {
+                log.debug("Dispatching switch update {} {}",
+                        sw, added);
+            }
+            if (switchListeners != null) {
+                for (IOFSwitchListener listener : switchListeners) {
+                    if (added)
+                        listener.addedSwitch(sw);
+                    else
+                        listener.removedSwitch(sw);
+                }
+            }
+        }
+    }
+    
+    /**
+     * Controller's role has changed
+     */
+    protected class HARoleUpdate implements IUpdate {
+        public Role oldRole;
+        public Role newRole;
+        public HARoleUpdate(Role newRole, Role oldRole) {
             this.oldRole = oldRole;
             this.newRole = newRole;
-            this.type = UpdateType.HA;
+        }
+        public void dispatch() {
+            if (log.isDebugEnabled()) {
+                log.debug("Dispatching HA Role update newRole = {}, oldRole = {}",
+                          newRole, oldRole);
+            }
+            if (haListeners != null) {
+                for (IHAListener listener : haListeners) {
+                        listener.roleChanged(oldRole, newRole);
+                }
+            }
+        }
+    }
+    
+    /**
+     * IPs of controllers in controller cluster have changed.
+     */
+    protected class HAControllerNodeIPUpdate implements IUpdate {
+        public Map<String,String> curControllerNodeIPs;
+        public Map<String,String> addedControllerNodeIPs;
+        public Map<String,String> removedControllerNodeIPs;
+        public HAControllerNodeIPUpdate(
+                HashMap<String,String> curControllerNodeIPs,  
+                HashMap<String,String> addedControllerNodeIPs,  
+                HashMap<String,String> removedControllerNodeIPs) {
+            this.curControllerNodeIPs = curControllerNodeIPs;
+            this.addedControllerNodeIPs = addedControllerNodeIPs;
+            this.removedControllerNodeIPs = removedControllerNodeIPs;
+        }
+        public void dispatch() {
+            if (log.isDebugEnabled()) {
+                log.debug("Dispatching HA Controller Node IP update "
+                        + "curIPs = {}, addedIPs = {}, removedIPs = {}",
+                        new Object[] { curControllerNodeIPs, addedControllerNodeIPs,
+                            removedControllerNodeIPs }
+                        );
+            }
+            if (haListeners != null) {
+                for (IHAListener listener: haListeners) {
+                    listener.controllerNodeIPsChanged(curControllerNodeIPs,
+                            addedControllerNodeIPs, removedControllerNodeIPs);
+                }
+            }
         }
     }
     
@@ -275,7 +357,11 @@ public class Controller implements IFloodlightProviderService {
         }
         
         // Enqueue an update for our listeners.
-        this.updates.add(new Update(role, oldRole));
+        try {
+            this.updates.put(new HARoleUpdate(role, oldRole));
+        } catch (InterruptedException e) {
+            log.error("Failure adding update to queue", e);
+        }
     }
     
     /**
@@ -907,39 +993,39 @@ public class Controller implements IFloodlightProviderService {
     /**
      * flcontext_cache - Keep a thread local stack of contexts
      */
-	protected static final ThreadLocal<Stack<FloodlightContext>> flcontext_cache =
-		new ThreadLocal <Stack<FloodlightContext>> () {
-			@Override
-			protected Stack<FloodlightContext> initialValue() {
-				return new Stack<FloodlightContext>();
-			}
-		};
+    protected static final ThreadLocal<Stack<FloodlightContext>> flcontext_cache =
+        new ThreadLocal <Stack<FloodlightContext>> () {
+            @Override
+            protected Stack<FloodlightContext> initialValue() {
+                return new Stack<FloodlightContext>();
+            }
+        };
 
-	/**
-	 * flcontext_alloc - pop a context off the stack, if required create a new one
-	 * @return FloodlightContext
-	 */
-	protected static FloodlightContext flcontext_alloc() {
-		FloodlightContext flcontext = null;
+    /**
+     * flcontext_alloc - pop a context off the stack, if required create a new one
+     * @return FloodlightContext
+     */
+    protected static FloodlightContext flcontext_alloc() {
+        FloodlightContext flcontext = null;
 
-		if (flcontext_cache.get().empty()) {
-			flcontext = new FloodlightContext();
-		}
-		else {
-			flcontext = flcontext_cache.get().pop();
-		}
+        if (flcontext_cache.get().empty()) {
+            flcontext = new FloodlightContext();
+        }
+        else {
+            flcontext = flcontext_cache.get().pop();
+        }
 
-		return flcontext;
-	}
+        return flcontext;
+    }
 
-	/**
-	 * flcontext_free - Free the context to the current thread
-	 * @param flcontext
-	 */
-	protected void flcontext_free(FloodlightContext flcontext) {
-		flcontext.getStorage().clear();
-		flcontext_cache.get().push(flcontext);
-	}
+    /**
+     * flcontext_free - Free the context to the current thread
+     * @param flcontext
+     */
+    protected void flcontext_free(FloodlightContext flcontext) {
+        flcontext.getStorage().clear();
+        flcontext_cache.get().push(flcontext);
+    }
 
     /**
      * Handle replies to certain OFMessages, and pass others off to listeners
@@ -1200,7 +1286,7 @@ public class Controller implements IFloodlightProviderService {
         }
         
         updateActiveSwitchInfo(sw);
-        Update update = new Update(sw, true);
+        SwitchUpdate update = new SwitchUpdate(sw, true);
         try {
             this.updates.put(update);
         } catch (InterruptedException e) {
@@ -1233,7 +1319,7 @@ public class Controller implements IFloodlightProviderService {
         // of the switch state that's written to storage.
         
         updateInactiveSwitchInfo(sw);
-        Update update = new Update(sw, false);
+        SwitchUpdate update = new SwitchUpdate(sw, false);
         try {
             this.updates.put(update);
         } catch (InterruptedException e) {
@@ -1257,7 +1343,7 @@ public class Controller implements IFloodlightProviderService {
         ldd.addListener(type, listener);
         
         if (log.isDebugEnabled()) {
-        	logListeners(type, ldd);
+            logListeners(type, ldd);
         }
     }
 
@@ -1269,7 +1355,7 @@ public class Controller implements IFloodlightProviderService {
         if (ldd != null) {
             ldd.removeListener(listener);
             if (log.isDebugEnabled()) {
-            	logListeners(type, ldd);
+                logListeners(type, ldd);
             }
         }
     }
@@ -1564,10 +1650,10 @@ public class Controller implements IFloodlightProviderService {
         Role role = null;
         String roleString = configParams.get("role");
         if (roleString == null) {
-        	String rolePath = configParams.get("rolepath");
-        	if (rolePath != null) {
-        		Properties properties = new Properties();
-        		try {
+            String rolePath = configParams.get("rolepath");
+            if (rolePath != null) {
+                Properties properties = new Properties();
+                try {
                     properties.load(new FileInputStream(rolePath));
                     roleString = properties.getProperty("floodlight.role");
                 }
@@ -1621,37 +1707,8 @@ public class Controller implements IFloodlightProviderService {
         // main loop
         while (true) {
             try {
-                Update update = updates.take();
-                switch (update.type) {
-                    case SWITCH:
-                        if (log.isDebugEnabled()) {
-                            log.debug("Dispatching switch update {} {}",
-                                      update.sw, update.added);
-                        }
-                        if (switchListeners != null) {
-                            for (IOFSwitchListener listener : switchListeners) {
-                                if (update.added)
-                                    listener.addedSwitch(update.sw);
-                                else
-                                    listener.removedSwitch(update.sw);
-                            }
-                        }
-                        break;
-                    case HA:
-                        if (log.isDebugEnabled()) {
-                            log.debug("Dispatching HA update newRole = {}, oldRole = {}",
-                                      update.newRole, update.oldRole);
-                        }
-                        if (haListeners != null) {
-                            for (IHARoleListener listener : haListeners) {
-                                    listener.roleChanged(update.oldRole, update.newRole);
-                            }
-                        }
-                        break;
-                    default:
-                        log.error("Unreognized update type " + update.type);
-                        break;
-                }
+                IUpdate update = updates.take();
+                update.dispatch();
             } catch (InterruptedException e) {
                 return;
             } catch (StorageException e) {
@@ -1727,10 +1784,11 @@ public class Controller implements IFloodlightProviderService {
                                       ListenerDispatcher<OFType, 
                                                          IOFMessageListener>>();
         this.switchListeners = new CopyOnWriteArraySet<IOFSwitchListener>();
-        this.haListeners = new CopyOnWriteArraySet<IHARoleListener>();
+        this.haListeners = new CopyOnWriteArraySet<IHAListener>();
         this.activeSwitches = new ConcurrentHashMap<Long, IOFSwitch>();
         this.connectedSwitches = new ConcurrentHashMap<Long, IOFSwitch>();
-        this.updates = new LinkedBlockingQueue<Update>();
+        this.controllerNodeIPsCache = new HashMap<String, String>();
+        this.updates = new LinkedBlockingQueue<IUpdate>();
         this.factory = new BasicFactory();
         this.providerMap = new HashMap<String, List<IInfoProvider>>();
         setConfigParams(configParams);
@@ -1747,11 +1805,15 @@ public class Controller implements IFloodlightProviderService {
         storageSource.createTable(CONTROLLER_TABLE_NAME, null);
         storageSource.createTable(SWITCH_TABLE_NAME, null);
         storageSource.createTable(PORT_TABLE_NAME, null);
+        storageSource.createTable(CONTROLLER_INTERFACE_TABLE_NAME, null);
         storageSource.setTablePrimaryKeyName(CONTROLLER_TABLE_NAME,
                                              CONTROLLER_ID);
         storageSource.setTablePrimaryKeyName(SWITCH_TABLE_NAME,
                                              SWITCH_DATAPATH_ID);
         storageSource.setTablePrimaryKeyName(PORT_TABLE_NAME, PORT_ID);
+        storageSource.setTablePrimaryKeyName(CONTROLLER_INTERFACE_TABLE_NAME, 
+                                             CONTROLLER_INTERFACE_ID);
+        storageSource.addListener(CONTROLLER_INTERFACE_TABLE_NAME, this);
         
         while (true) {
             try {
@@ -1772,42 +1834,120 @@ public class Controller implements IFloodlightProviderService {
         restApi.addRestletRoutable(new CoreWebRoutable());
     }
 
-	@Override
-	public void addInfoProvider(String type, IInfoProvider provider) {
-		if (!providerMap.containsKey(type)) {
-			providerMap.put(type, new ArrayList<IInfoProvider>());
-		}
-		providerMap.get(type).add(provider);
-	}
+    @Override
+    public void addInfoProvider(String type, IInfoProvider provider) {
+        if (!providerMap.containsKey(type)) {
+            providerMap.put(type, new ArrayList<IInfoProvider>());
+        }
+        providerMap.get(type).add(provider);
+    }
 
-	@Override
-	public void removeInfoProvider(String type, IInfoProvider provider) {
-		if (!providerMap.containsKey(type)) {
-			log.debug("Provider type {} doesn't exist.", type);
-			return;
-		}
-		
-		providerMap.get(type).remove(provider);
-	}
-	
-	public Map<String, Object> getControllerInfo(String type) {
-		if (!providerMap.containsKey(type)) return null;
-		
-		Map<String, Object> result = new LinkedHashMap<String, Object>();
-		for (IInfoProvider provider : providerMap.get(type)) {
-			result.putAll(provider.getInfo(type));
-		}
-		
-		return result;
-	}
+    @Override
+    public void removeInfoProvider(String type, IInfoProvider provider) {
+        if (!providerMap.containsKey(type)) {
+            log.debug("Provider type {} doesn't exist.", type);
+            return;
+        }
+        
+        providerMap.get(type).remove(provider);
+    }
+    
+    public Map<String, Object> getControllerInfo(String type) {
+        if (!providerMap.containsKey(type)) return null;
+        
+        Map<String, Object> result = new LinkedHashMap<String, Object>();
+        for (IInfoProvider provider : providerMap.get(type)) {
+            result.putAll(provider.getInfo(type));
+        }
+        
+        return result;
+    }
 
     @Override
-    public void addHAListener(IHARoleListener listener) {
+    public void addHAListener(IHAListener listener) {
         this.haListeners.add(listener);
     }
 
     @Override
-    public void removeHAListener(IHARoleListener listener) {
+    public void removeHAListener(IHAListener listener) {
         this.haListeners.remove(listener);
     }
+    
+    
+    /**
+     * Handle changes to the controller nodes IPs and dispatch update. 
+     */
+    @SuppressWarnings("unchecked")
+    protected void handleControllerNodeIPChanges() {
+        HashMap<String,String> curControllerNodeIPs = new HashMap<String,String>();
+        HashMap<String,String> addedControllerNodeIPs = new HashMap<String,String>();
+        HashMap<String,String> removedControllerNodeIPs =new HashMap<String,String>();
+        String[] colNames = { CONTROLLER_INTERFACE_CONTROLLER_ID, 
+                           CONTROLLER_INTERFACE_TYPE, 
+                           CONTROLLER_INTERFACE_NUMBER, 
+                           CONTROLLER_INTERFACE_DISCOVERED_IP };
+        synchronized(curControllerNodeIPs) {
+            // We currently assume that interface Ethernet0 is the relevant
+            // controller interface. Might change.
+            // We could (should?) implement this using 
+            // predicates, but creating the individual and compound predicate
+            // seems more overhead then just checking every row. Particularly, 
+            // since the number of rows is small and changes infrequent
+            IResultSet res = storageSource.executeQuery(CONTROLLER_INTERFACE_TABLE_NAME,
+                    colNames,null, null);
+            while (res.next()) {
+                if (res.getString(CONTROLLER_INTERFACE_TYPE).equals("Ethernet") &&
+                        res.getInt(CONTROLLER_INTERFACE_NUMBER) == 0) {
+                    String controllerID = res.getString(CONTROLLER_INTERFACE_CONTROLLER_ID);
+                    String discoveredIP = res.getString(CONTROLLER_INTERFACE_DISCOVERED_IP);
+                    String curIP = controllerNodeIPsCache.get(controllerID);
+                    
+                    curControllerNodeIPs.put(controllerID, discoveredIP);
+                    if (curIP == null) {
+                        // new controller node IP
+                        addedControllerNodeIPs.put(controllerID, discoveredIP);
+                    } 
+                    else if (curIP != discoveredIP) {
+                        // IP changed                    
+                        removedControllerNodeIPs.put(controllerID, curIP);
+                        addedControllerNodeIPs.put(controllerID, discoveredIP);
+                    }
+                }
+            }
+            // Now figure out if rows have been deleted. We can't use the
+            // rowKeys from rowsDeleted directly, since the tables primary
+            // key is a compound that we can't disassemble
+            Set<String> curEntries = curControllerNodeIPs.keySet();
+            Set<String> removedEntries = controllerNodeIPsCache.keySet();
+            removedEntries.removeAll(curEntries);
+            for (String removedControllerID : removedEntries)
+                removedControllerNodeIPs.put(removedControllerID, controllerNodeIPsCache.get(removedControllerID));
+            controllerNodeIPsCache = (HashMap<String, String>) curControllerNodeIPs.clone();
+            HAControllerNodeIPUpdate update = new HAControllerNodeIPUpdate(
+                                curControllerNodeIPs, addedControllerNodeIPs,
+                                removedControllerNodeIPs);
+            if (!removedControllerNodeIPs.isEmpty() || !addedControllerNodeIPs.isEmpty()) {
+                try {
+                    this.updates.put(update);
+                } catch (InterruptedException e) {
+                    log.error("Failure adding update to queue", e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void rowsModified(String tableName, Set<Object> rowKeys) {
+        if (tableName.equals(CONTROLLER_INTERFACE_TABLE_NAME)) {
+            handleControllerNodeIPChanges();
+        }
+        
+    }
+
+    @Override
+    public void rowsDeleted(String tableName, Set<Object> rowKeys) {
+        if (tableName.equals(CONTROLLER_INTERFACE_TABLE_NAME)) {
+            handleControllerNodeIPChanges();
+        }
+    }
 }
diff --git a/src/main/java/net/floodlightcontroller/linkdiscovery/internal/LinkDiscoveryManager.java b/src/main/java/net/floodlightcontroller/linkdiscovery/internal/LinkDiscoveryManager.java
index b1f06633cc409039cb87510b07dcb747d86e71c7..81fc0037a64ad662ef51d6a620c83a81c9d79d8e 100644
--- a/src/main/java/net/floodlightcontroller/linkdiscovery/internal/LinkDiscoveryManager.java
+++ b/src/main/java/net/floodlightcontroller/linkdiscovery/internal/LinkDiscoveryManager.java
@@ -42,7 +42,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
 import net.floodlightcontroller.core.FloodlightContext;
 import net.floodlightcontroller.core.IFloodlightProviderService;
 import net.floodlightcontroller.core.IFloodlightProviderService.Role;
-import net.floodlightcontroller.core.IHARoleListener;
+import net.floodlightcontroller.core.IHAListener;
 import net.floodlightcontroller.core.IInfoProvider;
 import net.floodlightcontroller.core.IOFMessageListener;
 import net.floodlightcontroller.core.IOFSwitch;
@@ -120,7 +120,7 @@ import org.slf4j.LoggerFactory;
 public class LinkDiscoveryManager
         implements IOFMessageListener, IOFSwitchListener, 
                    IStorageSourceListener, ILinkDiscoveryService,
-                   IFloodlightModule, IInfoProvider, IHARoleListener {
+                   IFloodlightModule, IInfoProvider, IHAListener {
     protected static Logger log = LoggerFactory.getLogger(LinkDiscoveryManager.class);
 
     // Names of table/fields for links in the storage API
@@ -1537,4 +1537,13 @@ public class LinkDiscoveryManager
                 break;
         }
     }
+    
+    @Override
+    public void controllerNodeIPsChanged(
+            Map<String, String> curControllerNodeIPs,
+            Map<String, String> addedControllerNodeIPs,
+            Map<String, String> removedControllerNodeIPs) {
+        // ignore
+    }
+    
 }
diff --git a/src/main/java/net/floodlightcontroller/staticflowentry/StaticFlowEntryPusher.java b/src/main/java/net/floodlightcontroller/staticflowentry/StaticFlowEntryPusher.java
index bff3a422fa9795bb15efb2892b0c15612adb3848..e2cc5ec2c38ecf738cecfcca846ba0701576148a 100644
--- a/src/main/java/net/floodlightcontroller/staticflowentry/StaticFlowEntryPusher.java
+++ b/src/main/java/net/floodlightcontroller/staticflowentry/StaticFlowEntryPusher.java
@@ -16,7 +16,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import net.floodlightcontroller.core.FloodlightContext;
 import net.floodlightcontroller.core.IFloodlightProviderService;
 import net.floodlightcontroller.core.IFloodlightProviderService.Role;
-import net.floodlightcontroller.core.IHARoleListener;
+import net.floodlightcontroller.core.IHAListener;
 import net.floodlightcontroller.core.IOFMessageListener;
 import net.floodlightcontroller.core.IOFSwitch;
 import net.floodlightcontroller.core.IOFSwitchListener;
@@ -46,7 +46,7 @@ import org.slf4j.LoggerFactory;
 
 public class StaticFlowEntryPusher 
     implements IOFSwitchListener, IFloodlightModule, IStaticFlowEntryPusherService,
-        IStorageSourceListener, IOFMessageListener, IHARoleListener {
+        IStorageSourceListener, IOFMessageListener, IHAListener {
     protected static Logger log = LoggerFactory.getLogger(StaticFlowEntryPusher.class);
     public static final String StaticFlowName = "staticflowentry";
     
@@ -606,7 +606,7 @@ public class StaticFlowEntryPusher
     }
 
     
-    // IHARoleListener
+    // IHAListener
     
     @Override
     public void roleChanged(Role oldRole, Role newRole) {
@@ -627,4 +627,13 @@ public class StaticFlowEntryPusher
                 break;
         }
     }
+    
+    @Override
+    public void controllerNodeIPsChanged(
+            Map<String, String> curControllerNodeIPs,
+            Map<String, String> addedControllerNodeIPs,
+            Map<String, String> removedControllerNodeIPs) {
+        // ignore
+    }
+     
 }
diff --git a/src/main/java/net/floodlightcontroller/topology/TopologyManager.java b/src/main/java/net/floodlightcontroller/topology/TopologyManager.java
index 762e19186f6c392c162ab70aaf6a97dca40ad898..eaef7d8f1eecb23ab4ab55b51c631b32e944a84b 100644
--- a/src/main/java/net/floodlightcontroller/topology/TopologyManager.java
+++ b/src/main/java/net/floodlightcontroller/topology/TopologyManager.java
@@ -13,7 +13,7 @@ import java.util.concurrent.TimeUnit;
 
 import net.floodlightcontroller.core.IFloodlightProviderService;
 import net.floodlightcontroller.core.IFloodlightProviderService.Role;
-import net.floodlightcontroller.core.IHARoleListener;
+import net.floodlightcontroller.core.IHAListener;
 import net.floodlightcontroller.core.module.FloodlightModuleContext;
 import net.floodlightcontroller.core.module.FloodlightModuleException;
 import net.floodlightcontroller.core.module.IFloodlightModule;
@@ -43,7 +43,7 @@ import org.slf4j.LoggerFactory;
 
 public class TopologyManager 
     implements IFloodlightModule, ITopologyService, IRoutingService, 
-               ILinkDiscoveryListener, IHARoleListener {
+               ILinkDiscoveryListener, IHAListener {
 
     protected static Logger log = LoggerFactory.getLogger(TopologyManager.class);
 
@@ -484,6 +484,8 @@ public class TopologyManager
         tunnelLinks.clear();
         createNewInstance();
     }
+    
+    // IHAListener
 
     @Override
     public void roleChanged(Role oldRole, Role newRole) {
@@ -502,6 +504,15 @@ public class TopologyManager
                 break;
         }
     }
+    
+    @Override
+    public void controllerNodeIPsChanged(
+            Map<String, String> curControllerNodeIPs,
+            Map<String, String> addedControllerNodeIPs,
+            Map<String, String> removedControllerNodeIPs) {
+        // ignore
+    }
+     
 
     @Override
     public Set<NodePortTuple> getBroadcastDomainLinks() {
diff --git a/src/test/java/net/floodlightcontroller/core/internal/ControllerTest.java b/src/test/java/net/floodlightcontroller/core/internal/ControllerTest.java
index 58b92ea9bf97b6d58cd8283654383f1a10d34b59..ef93bdad8cd15290fb16fb7e019b6b615ab6f336 100644
--- a/src/test/java/net/floodlightcontroller/core/internal/ControllerTest.java
+++ b/src/test/java/net/floodlightcontroller/core/internal/ControllerTest.java
@@ -24,6 +24,7 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
@@ -31,10 +32,13 @@ import java.util.concurrent.TimeUnit;
 import net.floodlightcontroller.core.FloodlightProvider;
 import net.floodlightcontroller.core.FloodlightContext;
 import net.floodlightcontroller.core.IFloodlightProviderService;
+import net.floodlightcontroller.core.IHAListener;
 import net.floodlightcontroller.core.IOFMessageFilterManagerService;
 import net.floodlightcontroller.core.IOFMessageListener;
+import net.floodlightcontroller.core.IFloodlightProviderService.Role;
 import net.floodlightcontroller.core.IOFMessageListener.Command;
 import net.floodlightcontroller.core.IOFSwitch;
+import net.floodlightcontroller.core.IOFSwitchListener;
 import net.floodlightcontroller.core.OFMessageFilterManager;
 import net.floodlightcontroller.core.module.FloodlightModuleContext;
 import net.floodlightcontroller.core.test.MockFloodlightProvider;
@@ -76,6 +80,7 @@ import org.openflow.protocol.statistics.OFStatisticsType;
  * @author David Erickson (daviderickson@cs.stanford.edu)
  */
 public class ControllerTest extends FloodlightTestCase {
+   
     private Controller controller;
     private MockThreadPoolService tp;
 
@@ -133,6 +138,16 @@ public class ControllerTest extends FloodlightTestCase {
             sr.setFlags((short) 1);
         return sr;
     }
+    
+    /**
+     * Run the controller's main loop so that updates are processed
+     */
+    protected class ControllerRunThread extends Thread {
+        public void run() {
+            controller.openFlowPort = 0; // Don't listen
+            controller.run();
+        }
+    }
 
     /**
      * Verify that a listener that throws an exception halts further
@@ -472,4 +487,178 @@ public class ControllerTest extends FloodlightTestCase {
 
         verify(newsw, channel, channel2);
     }
+    
+    @Test
+    public void testUpdateQueue() throws Exception {
+        class DummySwitchListener implements IOFSwitchListener {
+            public int nAdded;
+            public int nRemoved;
+            public DummySwitchListener() {
+                nAdded = 0;
+                nRemoved = 0;
+            }
+            public synchronized void addedSwitch(IOFSwitch sw) {
+                nAdded++;
+                notifyAll();
+            }
+            public synchronized void removedSwitch(IOFSwitch sw) {
+                nRemoved++;
+                notifyAll();
+            }
+            public String getName() {
+                return "dummy";
+            }
+        }
+        DummySwitchListener switchListener = new DummySwitchListener();
+        IOFSwitch sw = createMock(IOFSwitch.class);
+        ControllerRunThread t = new ControllerRunThread();
+        t.start();
+        
+        controller.addOFSwitchListener(switchListener);
+        synchronized(switchListener) {
+            controller.updates.put(controller.new SwitchUpdate(sw, true));
+            switchListener.wait(500);
+            assertTrue("IOFSwitchListener.addedSwitch() was not called", 
+                    switchListener.nAdded == 1);
+            controller.updates.put(controller.new SwitchUpdate(sw, false));
+            switchListener.wait(500);
+            assertTrue("IOFSwitchListener.removedSwitch() was not called", 
+                    switchListener.nRemoved == 1);
+        }
+    }
+    
+
+    private Map<String,Object> getFakeControllerIPRow(String id, String controllerId, 
+            String type, int number, String discoveredIP ) {
+        HashMap<String, Object> row = new HashMap<String,Object>();
+        row.put(Controller.CONTROLLER_INTERFACE_ID, id);
+        row.put(Controller.CONTROLLER_INTERFACE_CONTROLLER_ID, controllerId);
+        row.put(Controller.CONTROLLER_INTERFACE_TYPE, type);
+        row.put(Controller.CONTROLLER_INTERFACE_NUMBER, number);
+        row.put(Controller.CONTROLLER_INTERFACE_DISCOVERED_IP, discoveredIP);
+        return row;
+    }
+
+    /**
+     * Test notifications for controller node IP changes. This requires
+     * synchronization between the main test thread and another thread 
+     * that runs Controller's main loop and takes / handles updates. We
+     * synchronize with wait(timeout) / notifyAll(). We check for the 
+     * expected condition after the wait returns. However, if wait returns
+     * due to the timeout (or due to spurious awaking) and the check fails we
+     * might just not have waited long enough. Using a long enough timeout
+     * mitigates this but we cannot get rid of the fundamental "issue". 
+     * 
+     * @throws Exception
+     */
+    @Test
+    public void testControllerNodeIPChanges() throws Exception {
+        class DummyHAListener implements IHAListener {
+            public Map<String, String> curControllerNodeIPs;
+            public Map<String, String> addedControllerNodeIPs;
+            public Map<String, String> removedControllerNodeIPs;
+            public int nCalled;
+            
+            public DummyHAListener() {
+                this.nCalled = 0;
+            }
+                
+            @Override
+            public void roleChanged(Role oldRole, Role newRole) {
+                // ignore
+            }
+    
+            @Override
+            public synchronized void controllerNodeIPsChanged(
+                    Map<String, String> curControllerNodeIPs,
+                    Map<String, String> addedControllerNodeIPs,
+                    Map<String, String> removedControllerNodeIPs) {
+                this.curControllerNodeIPs = curControllerNodeIPs;
+                this.addedControllerNodeIPs = addedControllerNodeIPs;
+                this.removedControllerNodeIPs = removedControllerNodeIPs;
+                this.nCalled++;
+                notifyAll();
+            }
+            
+            public void do_assert(int nCalled,
+                    Map<String, String> curControllerNodeIPs,
+                    Map<String, String> addedControllerNodeIPs,
+                    Map<String, String> removedControllerNodeIPs) {
+                assertEquals("nCalled is not as expected", nCalled, this.nCalled);
+                assertEquals("curControllerNodeIPs is not as expected", 
+                        curControllerNodeIPs, this.curControllerNodeIPs);
+                assertEquals("addedControllerNodeIPs is not as expected", 
+                        addedControllerNodeIPs, this.addedControllerNodeIPs);
+                assertEquals("removedControllerNodeIPs is not as expected", 
+                        removedControllerNodeIPs, this.removedControllerNodeIPs);
+                
+            }
+        }
+        long waitTimeout = 250; // ms
+        DummyHAListener listener  = new DummyHAListener();
+        HashMap<String,String> expectedCurMap = new HashMap<String, String>();
+        HashMap<String,String> expectedAddedMap = new HashMap<String, String>();
+        HashMap<String,String> expectedRemovedMap = new HashMap<String, String>();
+        
+        controller.addHAListener(listener);
+        ControllerRunThread t = new ControllerRunThread();
+        t.start();
+        
+        synchronized(listener) {
+            // Insert a first entry
+            controller.storageSource.insertRow(Controller.CONTROLLER_INTERFACE_TABLE_NAME,
+                    getFakeControllerIPRow("row1", "c1", "Ethernet", 0, "1.1.1.1"));
+            expectedCurMap.clear();
+            expectedAddedMap.clear();
+            expectedRemovedMap.clear();
+            expectedCurMap.put("c1", "1.1.1.1");
+            expectedAddedMap.put("c1", "1.1.1.1");
+            listener.wait(waitTimeout);
+            listener.do_assert(1, expectedCurMap, expectedAddedMap, expectedRemovedMap);
+
+            // Add an interface that we want to ignore. 
+            controller.storageSource.insertRow(Controller.CONTROLLER_INTERFACE_TABLE_NAME,
+                    getFakeControllerIPRow("row2", "c1", "Ethernet", 1, "1.1.1.2"));
+            listener.wait(waitTimeout); // TODO: do a different check. This call will have to wait for the timeout
+            assertTrue("controllerNodeIPsChanged() should not have been called here", 
+                    listener.nCalled == 1);
+
+            // Add another entry
+            controller.storageSource.insertRow(Controller.CONTROLLER_INTERFACE_TABLE_NAME,
+                    getFakeControllerIPRow("row3", "c2", "Ethernet", 0, "2.2.2.2"));
+            expectedCurMap.clear();
+            expectedAddedMap.clear();
+            expectedRemovedMap.clear();
+            expectedCurMap.put("c1", "1.1.1.1");
+            expectedCurMap.put("c2", "2.2.2.2");
+            expectedAddedMap.put("c2", "2.2.2.2");
+            listener.wait(waitTimeout);
+            listener.do_assert(2, expectedCurMap, expectedAddedMap, expectedRemovedMap);
+
+
+            // Update an entry
+            controller.storageSource.updateRow(Controller.CONTROLLER_INTERFACE_TABLE_NAME,
+                    "row3", getFakeControllerIPRow("row3", "c2", "Ethernet", 0, "2.2.2.3"));
+            expectedCurMap.clear();
+            expectedAddedMap.clear();
+            expectedRemovedMap.clear();
+            expectedCurMap.put("c1", "1.1.1.1");
+            expectedCurMap.put("c2", "2.2.2.3");
+            expectedAddedMap.put("c2", "2.2.2.3");
+            expectedRemovedMap.put("c2", "2.2.2.2");
+            listener.wait(waitTimeout);
+            listener.do_assert(3, expectedCurMap, expectedAddedMap, expectedRemovedMap);
+
+            // Delete an entry
+            controller.storageSource.deleteRow(Controller.CONTROLLER_INTERFACE_TABLE_NAME, 
+                    "row3");
+            expectedCurMap.clear();
+            expectedAddedMap.clear();
+            expectedRemovedMap.clear();
+            expectedCurMap.put("c1", "1.1.1.1");
+            expectedRemovedMap.put("c2", "2.2.2.3");
+            listener.wait(waitTimeout);
+            listener.do_assert(4, expectedCurMap, expectedAddedMap, expectedRemovedMap);
+        }
+    }
 }
diff --git a/src/test/java/net/floodlightcontroller/core/test/MockFloodlightProvider.java b/src/test/java/net/floodlightcontroller/core/test/MockFloodlightProvider.java
index 56dc20fcba11f7448c5e8705cbad83c50ec77931..84a2cabd35f75b5bba8224c005f9fcbbf06f5298 100644
--- a/src/test/java/net/floodlightcontroller/core/test/MockFloodlightProvider.java
+++ b/src/test/java/net/floodlightcontroller/core/test/MockFloodlightProvider.java
@@ -27,7 +27,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
 
 import net.floodlightcontroller.core.FloodlightContext;
 import net.floodlightcontroller.core.IFloodlightProviderService;
-import net.floodlightcontroller.core.IHARoleListener;
+import net.floodlightcontroller.core.IHAListener;
 import net.floodlightcontroller.core.IInfoProvider;
 import net.floodlightcontroller.core.IOFMessageListener;
 import net.floodlightcontroller.core.IOFSwitch;
@@ -52,7 +52,7 @@ import org.openflow.protocol.factory.BasicFactory;
 public class MockFloodlightProvider implements IFloodlightModule, IFloodlightProviderService {
     protected Map<OFType, List<IOFMessageListener>> listeners;
     protected List<IOFSwitchListener> switchListeners;
-    protected List<IHARoleListener> haListeners;
+    protected List<IHAListener> haListeners;
     protected Map<Long, IOFSwitch> switches;
     protected BasicFactory factory;
 
@@ -63,7 +63,7 @@ public class MockFloodlightProvider implements IFloodlightModule, IFloodlightPro
         listeners = new ConcurrentHashMap<OFType, List<IOFMessageListener>>();
         switches = new ConcurrentHashMap<Long, IOFSwitch>();
         switchListeners = new CopyOnWriteArrayList<IOFSwitchListener>();
-        haListeners = new CopyOnWriteArrayList<IHARoleListener>();
+        haListeners = new CopyOnWriteArrayList<IHAListener>();
         factory = new BasicFactory();
     }
 
@@ -250,12 +250,12 @@ public class MockFloodlightProvider implements IFloodlightModule, IFloodlightPro
 	}
 
     @Override
-    public void addHAListener(IHARoleListener listener) {
+    public void addHAListener(IHAListener listener) {
         haListeners.add(listener);
     }
 
     @Override
-    public void removeHAListener(IHARoleListener listener) {
+    public void removeHAListener(IHAListener listener) {
         haListeners.remove(listener);
     }
     
@@ -275,7 +275,7 @@ public class MockFloodlightProvider implements IFloodlightModule, IFloodlightPro
      * @param newRole
      */
     public void dispatchRoleChanged(Role oldRole, Role newRole) {
-        for (IHARoleListener rl : haListeners) {
+        for (IHAListener rl : haListeners) {
             rl.roleChanged(oldRole, newRole);
         }
     }