NullDeviceProvider now takes configurations for :

  - Number of devices per ONOS instance.
  - Number of ports on the devices. This value is global.

The islands created by each instances' providers are joined together
in a chain (circle).

Reference : ONOS-860

Change-Id: I875ad1fbc1d4441869373c25de2ae5b62838e0d4
diff --git a/providers/null/device/pom.xml b/providers/null/device/pom.xml
index 18bfd3dd..345f551 100644
--- a/providers/null/device/pom.xml
+++ b/providers/null/device/pom.xml
@@ -31,4 +31,11 @@
 
     <description>ONOS Null protocol device provider</description>
 
+    <dependencies>
+      <dependency>
+        <groupId>org.osgi</groupId>
+        <artifactId>org.osgi.compendium</artifactId>
+      </dependency>
+    </dependencies>
+
 </project>
diff --git a/providers/null/device/src/main/java/org/onosproject/provider/nil/device/impl/NullDeviceProvider.java b/providers/null/device/src/main/java/org/onosproject/provider/nil/device/impl/NullDeviceProvider.java
index 3b650013..08f8260 100644
--- a/providers/null/device/src/main/java/org/onosproject/provider/nil/device/impl/NullDeviceProvider.java
+++ b/providers/null/device/src/main/java/org/onosproject/provider/nil/device/impl/NullDeviceProvider.java
@@ -15,16 +15,21 @@
  */
 package org.onosproject.provider.nil.device.impl;
 
+import static com.google.common.base.Strings.isNullOrEmpty;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Modified;
+import org.apache.felix.scr.annotations.Property;
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.onlab.packet.ChassisId;
 import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.NodeId;
 import org.onosproject.net.Device;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.MastershipRole;
@@ -39,10 +44,12 @@
 import org.onosproject.net.device.PortDescription;
 import org.onosproject.net.provider.AbstractProvider;
 import org.onosproject.net.provider.ProviderId;
+import org.osgi.service.component.ComponentContext;
 import org.slf4j.Logger;
 
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Dictionary;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
@@ -78,14 +85,22 @@
 
     //currently hardcoded. will be made configurable via rest/cli.
     private static final String SCHEME = "null";
-    private static final int NUMDEVICES = 10;
-    private static final int NUMPORTSPERDEVICE = 10;
+    private static final int DEF_NUMDEVICES = 10;
+    private static final int DEF_NUMPORTS = 10;
 
     //Delay between events in ms.
     private static final int EVENTINTERVAL = 5;
 
     private final Map<Integer, DeviceDescription> descriptions = Maps.newHashMap();
 
+    @Property(name = "devConfigs", value = "", label = "Instance-specific configurations")
+    private String devConfigs = "";
+
+    private int numDevices = DEF_NUMDEVICES;
+
+    @Property(name = "numPorts", intValue = DEF_NUMPORTS, label = "Number of ports per devices")
+    private int numPorts = DEF_NUMPORTS;
+
     private DeviceCreator creator;
 
 
@@ -99,15 +114,17 @@
     }
 
     @Activate
-    public void activate() {
+    public void activate(ComponentContext context) {
         providerService = providerRegistry.register(this);
-        deviceBuilder.submit(new DeviceCreator(true));
+        if (modified(context)) {
+            deviceBuilder.submit(new DeviceCreator(true));
+        }
         log.info("Started");
 
     }
 
     @Deactivate
-    public void deactivate() {
+    public void deactivate(ComponentContext context) {
         deviceBuilder.submit(new DeviceCreator(false));
         try {
             deviceBuilder.awaitTermination(1000, TimeUnit.MILLISECONDS);
@@ -121,6 +138,57 @@
         log.info("Stopped");
     }
 
+    @Modified
+    public boolean modified(ComponentContext context) {
+        if (context == null) {
+            log.info("No configuration file, using defaults: numDevices={}, numPorts={}",
+                    numDevices, numPorts);
+            return false;
+        }
+
+        Dictionary<?, ?> properties = context.getProperties();
+
+        int newDevNum = DEF_NUMDEVICES;
+        int newPortNum = DEF_NUMPORTS;
+        try {
+            String s = (String) properties.get("devConfigs");
+            if (!isNullOrEmpty(s)) {
+                newDevNum = getDevicesConfig(s);
+            }
+            s = (String) properties.get("numPorts");
+            newPortNum = isNullOrEmpty(s) ? DEF_NUMPORTS : Integer.valueOf(s.trim());
+        } catch (Exception e) {
+            log.warn(e.getMessage());
+        }
+
+        boolean chgd = false;
+        if (newDevNum != numDevices) {
+            numDevices = newDevNum;
+            chgd |= true;
+        }
+        if (newPortNum != numPorts) {
+            numPorts = newPortNum;
+            chgd |= true;
+        }
+        log.info("Using settings numDevices={}, numPorts={}", numDevices, numPorts);
+        return chgd;
+    }
+
+    private int getDevicesConfig(String config) {
+        for (String sub : config.split(",")) {
+            String[] params = sub.split(":");
+            if (params.length == 2) {
+                NodeId that = new NodeId(params[0].trim());
+                String nd = params[1];
+                if (clusterService.getLocalNode().id().equals(that)) {
+                    return Integer.valueOf(nd.trim());
+                }
+                continue;
+            }
+        }
+        return DEF_NUMDEVICES;
+    }
+
     @Override
     public void triggerProbe(DeviceId deviceId) {}
 
@@ -169,10 +237,13 @@
             ChassisId cid;
 
             // nodeIdHash takes into account for nodeID to avoid collisions when running multi-node providers.
-            int nodeIdHash = (clusterService.getLocalNode().hashCode() % NUMDEVICES) * NUMDEVICES;
+            long nodeIdHash = clusterService.getLocalNode().hashCode() << 16;
 
-            for (int i = nodeIdHash; i < nodeIdHash + NUMDEVICES; i++) {
-                did = DeviceId.deviceId(new URI(SCHEME, toHex(i), null));
+            for (int i = 0; i < numDevices; i++) {
+                // mark 'last' device to facilitate chaining of islands together
+                long id = (i + 1 == numDevices) ? nodeIdHash | 0xffff : nodeIdHash | i;
+
+                did = DeviceId.deviceId(new URI(SCHEME, toHex(id), null));
                 cid = new ChassisId(i);
                 DeviceDescription desc =
                         new DefaultDeviceDescription(did.uri(), Device.Type.SWITCH,
@@ -187,10 +258,10 @@
 
         private List<PortDescription> buildPorts() {
             List<PortDescription> ports = Lists.newArrayList();
-            for (int i = 0; i < NUMPORTSPERDEVICE; i++) {
+            for (int i = 0; i < numPorts; i++) {
                 ports.add(new DefaultPortDescription(PortNumber.portNumber(i), true,
                                                      Port.Type.COPPER,
-                                                     (long) 0));
+                                                     0));
             }
             return ports;
         }
diff --git a/providers/null/host/src/main/java/org/onosproject/provider/nil/host/impl/NullHostProvider.java b/providers/null/host/src/main/java/org/onosproject/provider/nil/host/impl/NullHostProvider.java
index 8f59bcb..e20bfca 100644
--- a/providers/null/host/src/main/java/org/onosproject/provider/nil/host/impl/NullHostProvider.java
+++ b/providers/null/host/src/main/java/org/onosproject/provider/nil/host/impl/NullHostProvider.java
@@ -132,7 +132,8 @@
         public void event(DeviceEvent event) {
             if (!deviceService.getRole(event.subject().id())
                     .equals(MastershipRole.MASTER)) {
-                log.info("Local node is not master for device", event.subject().id());
+                log.info("Local node is not master for device {}", event
+                        .subject().id());
                 return;
             }
             switch (event.type()) {
diff --git a/providers/null/link/src/main/java/org/onosproject/provider/nil/link/impl/NullLinkProvider.java b/providers/null/link/src/main/java/org/onosproject/provider/nil/link/impl/NullLinkProvider.java
index 1caac54..a90e61c 100644
--- a/providers/null/link/src/main/java/org/onosproject/provider/nil/link/impl/NullLinkProvider.java
+++ b/providers/null/link/src/main/java/org/onosproject/provider/nil/link/impl/NullLinkProvider.java
@@ -46,9 +46,12 @@
 import org.onosproject.net.device.DeviceService;
 import org.onosproject.net.link.DefaultLinkDescription;
 import org.onosproject.net.link.LinkDescription;
+import org.onosproject.net.link.LinkEvent;
+import org.onosproject.net.link.LinkListener;
 import org.onosproject.net.link.LinkProvider;
 import org.onosproject.net.link.LinkProviderRegistry;
 import org.onosproject.net.link.LinkProviderService;
+import org.onosproject.net.link.LinkService;
 import org.onosproject.net.provider.AbstractProvider;
 import org.onosproject.net.provider.ProviderId;
 import org.osgi.service.component.ComponentContext;
@@ -74,6 +77,7 @@
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected LinkProviderRegistry providerRegistry;
+    private LinkService linkService;
 
     private LinkProviderService providerService;
 
@@ -84,13 +88,16 @@
     private static final PortNumber DSTPORT = PortNumber.portNumber(6);
 
     private final InternalLinkProvider linkProvider = new InternalLinkProvider();
+    private final InternalLinkListener listener = new InternalLinkListener();
 
     // Link descriptions
     private final ConcurrentMap<ConnectPoint, LinkDescription> descriptions = Maps
             .newConcurrentMap();
 
-    // Device ID's that have been seen so far
+    // Local Device ID's that have been seen so far
     private final List<DeviceId> devices = Lists.newArrayList();
+    // tail ends of other islands
+    private final List<ConnectPoint> tails = Lists.newArrayList();
 
     private ExecutorService linkDriver = Executors.newFixedThreadPool(1,
             namedThreads("onos-null-link-driver"));
@@ -112,6 +119,8 @@
     @Activate
     public void activate(ComponentContext context) {
         providerService = providerRegistry.register(this);
+        linkService = (LinkService) providerRegistry;
+        linkService.addListener(listener);
         deviceService.addListener(linkProvider);
         modified(context);
         log.info("started");
@@ -129,7 +138,9 @@
         }
         deviceService.removeListener(linkProvider);
         providerRegistry.unregister(this);
+        linkService.removeListener(listener);
         deviceService = null;
+        linkService = null;
 
         log.info("stopped");
     }
@@ -170,6 +181,11 @@
                 eventRate);
     }
 
+    // pick out substring from Deviceid
+    private String part(String devId) {
+        return devId.split(":")[1].substring(12, 16);
+    }
+
     /**
      * Adds links as devices are found, and generates LinkEvents.
      */
@@ -178,9 +194,6 @@
         @Override
         public void event(DeviceEvent event) {
             Device dev = event.subject();
-            if (!MASTER.equals(roleService.getLocalRole(dev.id()))) {
-                return;
-            }
             switch (event.type()) {
             case DEVICE_ADDED:
                 addLink(dev);
@@ -194,16 +207,26 @@
         }
 
         private void addLink(Device current) {
-            devices.add(current.id());
-            // No link if only one device
+            DeviceId did = current.id();
+            if (!MASTER.equals(roleService.getLocalRole(did))) {
+                String part = part(did.toString());
+                if (part.equals("ffff")) {
+                    // 'tail' of an island - link us <- tail
+                    tails.add(new ConnectPoint(did, SRCPORT));
+                }
+                tryLinkTail();
+                return;
+            }
+            devices.add(did);
+
             if (devices.size() == 1) {
                 return;
             }
 
-            // Attach new device to the last-seen device
+            // Normal flow - attach new device to the last-seen device
             DeviceId prev = devices.get(devices.size() - 2);
             ConnectPoint src = new ConnectPoint(prev, SRCPORT);
-            ConnectPoint dst = new ConnectPoint(current.id(), DSTPORT);
+            ConnectPoint dst = new ConnectPoint(did, DSTPORT);
 
             LinkDescription fdesc = new DefaultLinkDescription(src, dst,
                     Link.Type.DIRECT);
@@ -216,10 +239,66 @@
             providerService.linkDetected(rdesc);
         }
 
+        // try to link to a tail to first element
+        private void tryLinkTail() {
+            if (tails.isEmpty() || devices.isEmpty()) {
+                return;
+            }
+            ConnectPoint first = new ConnectPoint(devices.get(0), DSTPORT);
+            boolean added = false;
+            for (ConnectPoint cp : tails) {
+                if (!linkService.getLinks(cp).isEmpty()) {
+                    continue;
+                }
+                LinkDescription ld = new DefaultLinkDescription(cp, first,
+                        Link.Type.DIRECT);
+                descriptions.put(cp, ld);
+                providerService.linkDetected(ld);
+                added = true;
+                break;
+            }
+            if (added) {
+                tails.clear();
+            }
+        }
+
         private void removeLink(Device device) {
+            if (!MASTER.equals(roleService.getLocalRole(device.id()))) {
+                return;
+            }
             providerService.linksVanished(device.id());
             devices.remove(device.id());
         }
+
+    }
+
+    private class InternalLinkListener implements LinkListener {
+
+        @Override
+        public void event(LinkEvent event) {
+            switch (event.type()) {
+            case LINK_ADDED:
+                // If a link from another island, cast one back.
+                DeviceId sdid = event.subject().src().deviceId();
+                PortNumber pn = event.subject().src().port();
+
+                if (roleService.getLocalRole(sdid).equals(MASTER)) {
+                    String part = part(sdid.toString());
+                    if (part.equals("ffff") && SRCPORT.equals(pn)) {
+                        LinkDescription ld = new DefaultLinkDescription(event
+                                .subject().dst(), event.subject().src(),
+                                Link.Type.DIRECT);
+                        descriptions.put(event.subject().dst(), ld);
+                        providerService.linkDetected(ld);
+                    }
+                    return;
+                }
+                break;
+            default:
+                break;
+            }
+        }
+
     }
 
     /**
diff --git a/tools/package/etc/org.onosproject.provider.nil.device.impl.NullDeviceProvider.cfg b/tools/package/etc/org.onosproject.provider.nil.device.impl.NullDeviceProvider.cfg
new file mode 100644
index 0000000..375ed48
--- /dev/null
+++ b/tools/package/etc/org.onosproject.provider.nil.device.impl.NullDeviceProvider.cfg
@@ -0,0 +1,11 @@
+#
+# Instance-specific configurations, in this case, the number of 
+# devices per node.
+#
+devConfigs = 192.168.56.30:5,192.168.56.40:7
+
+#
+# Number of ports per device. This is global to all devices
+# on all instances.
+#
+# numPorts = 8