CORD-633 Make controllers of the br-int configurable

Change-Id: Ie7b17d2be9d46dae201de0092183b5301ef899b3
diff --git a/src/main/java/org/opencord/cordvtn/api/Constants.java b/src/main/java/org/opencord/cordvtn/api/Constants.java
index 9fd3b2c..d6d2831 100644
--- a/src/main/java/org/opencord/cordvtn/api/Constants.java
+++ b/src/main/java/org/opencord/cordvtn/api/Constants.java
@@ -16,7 +16,6 @@
 package org.opencord.cordvtn.api;
 
 import org.onlab.packet.Ip4Address;
-import org.onlab.packet.TpPort;
 
 /**
  * Provides constants used in CORD VTN services.
@@ -28,8 +27,6 @@
 
     public static final String CORDVTN_APP_ID = "org.opencord.vtn";
 
-    public static final String ERROR_XOS_ACCESS = "XOS access is not configured";
-    public static final String ERROR_OPENSTACK_ACCESS = "OpenStack access is not configured";
     public static final String MSG_OK = "OK";
     public static final String MSG_NO = "NO";
 
@@ -37,8 +34,9 @@
     public static final String INTEGRATION_BRIDGE = "br-int";
     public static final String NOT_APPLICABLE = "N/A";
 
-    public static final int OF_PORT = 6653;
-    public static final TpPort OVSDB_PORT = TpPort.tpPort(6640);
+    public static final String DEFAULT_OF_PROTOCOL = "tcp";
+    public static final int DEFAULT_OF_PORT = 6653;
+    public static final int DEFAULT_OVSDB_PORT = 6640;
     public static final Ip4Address DEFAULT_DNS = Ip4Address.valueOf("8.8.8.8");
     public static final int DHCP_INFINITE_LEASE = -1;
 }
diff --git a/src/main/java/org/opencord/cordvtn/api/config/CordVtnConfig.java b/src/main/java/org/opencord/cordvtn/api/config/CordVtnConfig.java
index 3f2b094..7f5a102 100644
--- a/src/main/java/org/opencord/cordvtn/api/config/CordVtnConfig.java
+++ b/src/main/java/org/opencord/cordvtn/api/config/CordVtnConfig.java
@@ -18,23 +18,33 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import org.onlab.osgi.DefaultServiceDirectory;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.TpPort;
+import org.onosproject.cluster.ClusterService;
 import org.onosproject.core.ApplicationId;
+import org.onosproject.net.behaviour.ControllerInfo;
 import org.onosproject.net.config.Config;
+import org.onosproject.net.config.InvalidFieldException;
 import org.opencord.cordvtn.api.node.CordVtnNode;
 import org.opencord.cordvtn.api.node.NetworkAddress;
 import org.opencord.cordvtn.api.node.SshAccessInfo;
 import org.slf4j.Logger;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import static org.onosproject.net.config.Config.FieldPresence.MANDATORY;
 import static org.onosproject.net.config.Config.FieldPresence.OPTIONAL;
+import static org.opencord.cordvtn.api.Constants.DEFAULT_OF_PORT;
+import static org.opencord.cordvtn.api.Constants.DEFAULT_OF_PROTOCOL;
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
@@ -66,13 +76,18 @@
 
     private static final String OPENSTACK = "openstack";
     private static final String XOS = "xos";
-
     private static final String ENDPOINT = "endpoint";
     private static final String TENANT = "tenant";
     private static final String USER = "user";
     private static final String PASSWORD = "password";
 
-    // TODO implement isValid
+    private static final String CONTROLLERS = "controllers";
+    private static final int INDEX_IP = 0;
+    private static final int INDEX_PORT = 1;
+
+    private final ClusterService clusterService =
+            DefaultServiceDirectory.getService(ClusterService.class);
+
     @Override
     public boolean isValid() {
         // check only allowed fields are present
@@ -84,7 +99,8 @@
                 SSH,
                 OPENSTACK,
                 XOS,
-                CORDVTN_NODES);
+                CORDVTN_NODES,
+                CONTROLLERS);
 
         if (object.get(CORDVTN_NODES) == null || object.get(CORDVTN_NODES).size() < 1) {
             final String msg = "No node is present";
@@ -92,20 +108,20 @@
         }
 
         // check all mandatory fields are present and valid
-        result = result && isMacAddress(PRIVATE_GATEWAY_MAC, MANDATORY);
-        result = result && isIpPrefix(LOCAL_MANAGEMENT_IP, MANDATORY);
+        result &= isMacAddress(PRIVATE_GATEWAY_MAC, MANDATORY);
+        result &= isIpPrefix(LOCAL_MANAGEMENT_IP, MANDATORY);
 
         for (JsonNode node : object.get(CORDVTN_NODES)) {
             ObjectNode vtnNode = (ObjectNode) node;
-            result = result && hasFields(
+            result &= hasFields(
                     vtnNode,
                     HOSTNAME,
                     HOST_MANAGEMENT_IP,
                     DATA_IP,
                     DATA_IFACE,
                     INTEGRATION_BRIDGE_ID);
-            result = result && isIpPrefix(vtnNode, HOST_MANAGEMENT_IP, MANDATORY);
-            result = result && isIpPrefix(vtnNode, DATA_IP, MANDATORY);
+            result &= isIpPrefix(vtnNode, HOST_MANAGEMENT_IP, MANDATORY);
+            result &= isIpPrefix(vtnNode, DATA_IP, MANDATORY);
 
             NetworkAddress localMgmt = NetworkAddress.valueOf(get(LOCAL_MANAGEMENT_IP, ""));
             NetworkAddress hostsMgmt = NetworkAddress.valueOf(getConfig(vtnNode, HOST_MANAGEMENT_IP));
@@ -116,42 +132,74 @@
             }
         }
 
-        result = result && hasFields(
+        result &= hasFields(
                 (ObjectNode) object.get(SSH),
                 SSH_PORT,
                 SSH_USER,
                 SSH_KEY_FILE);
-        result = result && isTpPort(
+        result &= isTpPort(
                 (ObjectNode) object.get(SSH),
                 SSH_PORT,
                 MANDATORY);
 
-        result = result && hasFields(
+        result &= hasFields(
                 (ObjectNode) object.get(OPENSTACK),
                 ENDPOINT,
                 TENANT,
                 USER,
                 PASSWORD);
 
-        result = result && hasFields(
+        result &= hasFields(
                 (ObjectNode) object.get(XOS),
                 ENDPOINT,
                 USER,
                 PASSWORD);
 
         // check all optional fields are valid
-        result = result && isTpPort(OVSDB_PORT, OPTIONAL);
+        result &= isTpPort(OVSDB_PORT, OPTIONAL);
 
         if (object.get(PUBLIC_GATEWAYS) != null && object.get(PUBLIC_GATEWAYS).isArray()) {
             for (JsonNode node : object.get(PUBLIC_GATEWAYS)) {
                 ObjectNode gateway = (ObjectNode) node;
-                result = result && isIpAddress(gateway, GATEWAY_IP, MANDATORY);
-                result = result && isMacAddress(gateway, GATEWAY_MAC, MANDATORY);
+                result &= isIpAddress(gateway, GATEWAY_IP, MANDATORY);
+                result &= isMacAddress(gateway, GATEWAY_MAC, MANDATORY);
+            }
+        }
+
+        if (object.get(CONTROLLERS) != null) {
+            for (JsonNode jsonNode : object.get(CONTROLLERS)) {
+                result &= isController(jsonNode);
             }
         }
         return result;
     }
 
+    private boolean isController(JsonNode jsonNode) {
+        String[] ctrl = jsonNode.asText().split(":");
+        final String error = "Malformed controller string " + jsonNode.asText() +
+                ". Controller only takes a list of 'IP:port', 'IP', " +
+                "or just one ':port'.";
+        try {
+            if (ctrl.length == 1) {
+                IpAddress.valueOf(ctrl[INDEX_IP]);
+                return true;
+            }
+            if (ctrl.length == 2 && ctrl[INDEX_IP].isEmpty() &&
+                    object.get(CONTROLLERS).size() == 1) {
+                TpPort.tpPort(Integer.valueOf(ctrl[INDEX_PORT]));
+                return true;
+            }
+            if (ctrl.length == 2 && !ctrl[INDEX_IP].isEmpty()) {
+                IpAddress.valueOf(ctrl[INDEX_IP]);
+                TpPort.tpPort(Integer.valueOf(ctrl[INDEX_PORT]));
+                return true;
+            }
+            throw new InvalidFieldException(CONTROLLERS, error);
+        } catch (IllegalArgumentException e) {
+            throw new InvalidFieldException(CONTROLLERS, error);
+        }
+    }
+
     /**
      * Returns the set of nodes read from network config.
      *
@@ -261,4 +309,50 @@
                                    jsonNode.path(USER).asText(),
                                    jsonNode.path(PASSWORD).asText());
     }
+
+    /**
+     * Returns controllers for the integration bridge.
+     * It returns the information taken from cluster service with the default OF
+     * port if no controller is specified in the network config.
+     *
+     * @return list of controller information
+     */
+    public List<ControllerInfo> controllers() {
+        List<ControllerInfo> ctrls = Lists.newArrayList();
+        JsonNode ctrlNodes = object.get(CONTROLLERS);
+
+        if (ctrlNodes == null || isCtrlPortOnly()) {
+            ctrls = clusterService.getNodes().stream()
+                    .map(ctrl -> new ControllerInfo(
+                            ctrl.ip(),
+                            ctrlNodes == null ? DEFAULT_OF_PORT : getCtrlPort(),
+                            DEFAULT_OF_PROTOCOL))
+                    .collect(Collectors.toList());
+        } else {
+            for (JsonNode ctrlNode : ctrlNodes) {
+                String[] ctrl = ctrlNode.asText().split(":");
+                ctrls.add(new ControllerInfo(
+                        IpAddress.valueOf(ctrl[INDEX_IP]),
+                        ctrl.length == 1 ? DEFAULT_OF_PORT :
+                                Integer.parseInt(ctrl[INDEX_PORT]),
+                        DEFAULT_OF_PROTOCOL));
+            }
+        }
+        return ImmutableList.copyOf(ctrls);
+    }
+
+    private boolean isCtrlPortOnly() {
+        if (object.get(CONTROLLERS).size() != 1) {
+            return false;
+        }
+        JsonNode jsonNode = object.get(CONTROLLERS).get(0);
+        String[] ctrl = jsonNode.asText().split(":");
+        return ctrl.length == 2 && ctrl[INDEX_IP].isEmpty();
+    }
+
+    private int getCtrlPort() {
+        JsonNode jsonNode = object.get(CONTROLLERS).get(0);
+        String[] ctrl = jsonNode.asText().split(":");
+        return Integer.parseInt(ctrl[INDEX_PORT]);
+    }
 }
diff --git a/src/main/java/org/opencord/cordvtn/api/node/CordVtnNode.java b/src/main/java/org/opencord/cordvtn/api/node/CordVtnNode.java
index 2c3916c..d4339df 100644
--- a/src/main/java/org/opencord/cordvtn/api/node/CordVtnNode.java
+++ b/src/main/java/org/opencord/cordvtn/api/node/CordVtnNode.java
@@ -28,8 +28,8 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static org.opencord.cordvtn.api.Constants.DEFAULT_OVSDB_PORT;
 import static org.opencord.cordvtn.api.Constants.DEFAULT_TUNNEL;
-import static org.opencord.cordvtn.api.Constants.OVSDB_PORT;
 
 /**
  * Representation of a compute infrastructure node for CORD VTN service.
@@ -149,7 +149,7 @@
         if (this.ovsdbPort.isPresent()) {
             return this.ovsdbPort.get();
         } else {
-            return OVSDB_PORT;
+            return TpPort.tpPort(DEFAULT_OVSDB_PORT);
         }
     }
 
@@ -289,7 +289,8 @@
         private NetworkAddress hostMgmtIp;
         private NetworkAddress localMgmtIp;
         private NetworkAddress dataIp;
-        private Optional<TpPort> ovsdbPort = Optional.of(OVSDB_PORT);
+        private Optional<TpPort> ovsdbPort =
+                Optional.of(TpPort.tpPort(DEFAULT_OVSDB_PORT));
         private SshAccessInfo sshInfo;
         private DeviceId integrationBridgeId;
         private String dataIface;
diff --git a/src/main/java/org/opencord/cordvtn/impl/CordVtnNodeManager.java b/src/main/java/org/opencord/cordvtn/impl/CordVtnNodeManager.java
index 78cc228..c2a89b9 100644
--- a/src/main/java/org/opencord/cordvtn/impl/CordVtnNodeManager.java
+++ b/src/main/java/org/opencord/cordvtn/impl/CordVtnNodeManager.java
@@ -16,6 +16,7 @@
 package org.opencord.cordvtn.impl;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
 import com.jcraft.jsch.Session;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
@@ -170,6 +171,7 @@
     private final BridgeHandler bridgeHandler = new BridgeHandler();
 
     private ConsistentMap<String, CordVtnNode> nodeStore;
+    private List<ControllerInfo> controllers = Lists.newArrayList();
     private ApplicationId appId;
     private NodeId localNodeId;
 
@@ -226,10 +228,9 @@
     @Activate
     protected void activate() {
         appId = coreService.registerApplication(CORDVTN_APP_ID);
-
-        configRegistry.registerConfigFactory(configFactory);
-        localNodeId = clusterService.getLocalNode().id();
         leadershipService.runForLeadership(appId.name());
+        localNodeId = clusterService.getLocalNode().id();
+        configRegistry.registerConfigFactory(configFactory);
 
         nodeStore = storageService.<String, CordVtnNode>consistentMapBuilder()
                 .withSerializer(Serializer.using(NODE_SERIALIZER.build()))
@@ -241,6 +242,8 @@
         deviceService.addListener(deviceListener);
         configService.addListener(configListener);
 
+        // TODO read nodes as well after more tests
+        readControllers();
         log.info("Started");
     }
 
@@ -329,7 +332,7 @@
     public PortNumber dataPort(DeviceId deviceId) {
         CordVtnNode node = nodeByBridgeId(deviceId);
         if (node == null) {
-            log.warn("Failed to get node for {}", deviceId);
+            log.debug("Failed to get node for {}", deviceId);
             return null;
         }
 
@@ -346,7 +349,7 @@
     public IpAddress dataIp(DeviceId deviceId) {
         CordVtnNode node = nodeByBridgeId(deviceId);
         if (node == null) {
-            log.warn("Failed to get node for {}", deviceId);
+            log.debug("Failed to get node for {}", deviceId);
             return null;
         }
         return node.dataIp().ip();
@@ -372,7 +375,7 @@
     public PortNumber hostManagementPort(DeviceId deviceId) {
         CordVtnNode node = nodeByBridgeId(deviceId);
         if (node == null) {
-            log.warn("Failed to get node for {}", deviceId);
+            log.debug("Failed to get node for {}", deviceId);
             return null;
         }
 
@@ -540,10 +543,6 @@
             return;
         }
 
-        List<ControllerInfo> controllers = clusterService.getNodes().stream()
-                .map(controller -> new ControllerInfo(controller.ip(), OF_PORT, "tcp"))
-                .collect(Collectors.toList());
-
         String dpid = node.integrationBridgeId().toString().substring(DPID_BEGIN);
         BridgeDescription bridgeDesc = DefaultBridgeDescription.builder()
                 .name(INTEGRATION_BRIDGE)
@@ -844,7 +843,13 @@
     /**
      * Reads cordvtn nodes from config file.
      */
-    private void readConfiguration() {
+    private void readNodes() {
+        NodeId leaderNodeId = leadershipService.getLeader(appId.name());
+        if (!Objects.equals(localNodeId, leaderNodeId)) {
+            // do not allow to proceed without leadership
+            return;
+        }
+
         CordVtnConfig config = configRegistry.getConfig(appId, CordVtnConfig.class);
         if (config == null) {
             log.debug("No configuration found");
@@ -853,16 +858,22 @@
         config.cordVtnNodes().forEach(this::addOrUpdateNode);
     }
 
+    private void readControllers() {
+        CordVtnConfig config = configRegistry.getConfig(appId, CordVtnConfig.class);
+        if (config == null) {
+            log.debug("No configuration found");
+            return;
+        }
+        controllers = config.controllers();
+        controllers.stream().forEach(ctrl -> {
+            log.debug("Added controller {}:{}", ctrl.ip(), ctrl.port());
+        });
+    }
+
     private class InternalConfigListener implements NetworkConfigListener {
 
         @Override
         public void event(NetworkConfigEvent event) {
-            NodeId leaderNodeId = leadershipService.getLeader(appId.name());
-            if (!Objects.equals(localNodeId, leaderNodeId)) {
-                // do not allow to proceed without leadership
-                return;
-            }
-
             if (!event.configClass().equals(CordVtnConfig.class)) {
                 return;
             }
@@ -870,7 +881,10 @@
             switch (event.type()) {
                 case CONFIG_ADDED:
                 case CONFIG_UPDATED:
-                    eventExecutor.execute(CordVtnNodeManager.this::readConfiguration);
+                    eventExecutor.execute(() -> {
+                        readControllers();
+                        readNodes();
+                    });
                     break;
                 default:
                     break;